mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 07:56:50 +02:00
fix: ensure compact previews properly truncate RichText to single line
- Add line-clamp-1 to all compact preview components for consistent truncation - Fix DefaultCompactPreview, GenericRepostCompactPreview, ZapCompactPreview - Fix Kind1111Renderer parent event preview truncation - Apply auto-formatting fixes from linter
This commit is contained in:
@@ -43,7 +43,7 @@ export function ShareSpellbookDialog({
|
||||
|
||||
// Get relays from event or fallback to author's outbox relays
|
||||
let relays = event.tags.filter((t) => t[0] === "r").map((t) => t[1]);
|
||||
|
||||
|
||||
if (relays.length === 0) {
|
||||
const authorRelays = await relayListCache.getOutboxRelays(event.pubkey);
|
||||
if (authorRelays) {
|
||||
@@ -149,4 +149,4 @@ export function ShareSpellbookDialog({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,13 +390,13 @@ export function SpellbooksViewer() {
|
||||
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(
|
||||
|
||||
@@ -70,9 +70,11 @@ export function AppShell({ children }: AppShellProps) {
|
||||
|
||||
<UserMenu />
|
||||
</header>
|
||||
<section className="flex-1 relative overflow-hidden">{children}</section>
|
||||
<section className="flex-1 relative overflow-hidden">
|
||||
{children}
|
||||
</section>
|
||||
<TabBar />
|
||||
</main>
|
||||
</AppShellContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +363,8 @@ export function MediaEmbed({
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3",
|
||||
onAudioClick && "cursor-crosshair hover:opacity-80 transition-opacity",
|
||||
onAudioClick &&
|
||||
"cursor-crosshair hover:opacity-80 transition-opacity",
|
||||
className,
|
||||
)}
|
||||
onClick={onAudioClick ? handleAudioClick : undefined}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function GenericRepostCompactPreview({ event }: { event: NostrEvent }) {
|
||||
pubkey={repostedEvent.pubkey}
|
||||
className="text-sm shrink-0"
|
||||
/>
|
||||
<span className="truncate">
|
||||
<span className="truncate line-clamp-1">
|
||||
<RichText
|
||||
content={preview || ""}
|
||||
className="inline text-sm leading-none"
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ZapCompactPreview({ event }: { event: NostrEvent }) {
|
||||
{amountInSats.toLocaleString("en", { notation: "compact" })}
|
||||
</span>
|
||||
{zapMessage && (
|
||||
<span className="flex-1">
|
||||
<span className="flex-1 truncate line-clamp-1">
|
||||
<RichText
|
||||
content={zapMessage}
|
||||
className="inline text-sm leading-none"
|
||||
@@ -65,7 +65,7 @@ export function ZapCompactPreview({ event }: { event: NostrEvent }) {
|
||||
pubkey={zappedEvent.pubkey}
|
||||
className="text-sm truncate line-clamp-1"
|
||||
/>
|
||||
<span className="text-muted-foreground truncate">
|
||||
<span className="text-muted-foreground truncate line-clamp-1">
|
||||
<RichText
|
||||
content={preview || ""}
|
||||
className="inline text-sm leading-none"
|
||||
|
||||
@@ -76,7 +76,7 @@ export function DefaultCompactPreview({ event }: { event: NostrEvent }) {
|
||||
: title;
|
||||
|
||||
return (
|
||||
<span className="truncate text-muted-foreground text-sm">
|
||||
<span className="truncate line-clamp-1 text-muted-foreground text-sm">
|
||||
<RichText
|
||||
content={displayText}
|
||||
className="inline text-sm leading-none"
|
||||
|
||||
@@ -79,7 +79,7 @@ function ParentEventCard({
|
||||
pubkey={parentEvent.pubkey}
|
||||
className="text-accent font-semibold flex-shrink-0"
|
||||
/>
|
||||
<div className="text-muted-foreground truncate min-w-0 flex-1">
|
||||
<div className="text-muted-foreground truncate line-clamp-1 min-w-0 flex-1">
|
||||
{getEventDisplayTitle(parentEvent, false) || (
|
||||
<RichText
|
||||
event={parentEvent}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function SpellbookPage() {
|
||||
// 1. Resolve actor to pubkey
|
||||
useEffect(() => {
|
||||
if (!actor) {
|
||||
// Should not happen in this route, but safe guard
|
||||
// Should not happen in this route, but safe guard
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,8 +101,10 @@ export default function SpellbookPage() {
|
||||
useEffect(() => {
|
||||
if (spellbookEvent && !hasLoadedSpellbook) {
|
||||
try {
|
||||
const parsedSpellbook = parseSpellbook(spellbookEvent as SpellbookEvent);
|
||||
|
||||
const parsedSpellbook = parseSpellbook(
|
||||
spellbookEvent as SpellbookEvent,
|
||||
);
|
||||
|
||||
const isPreviewPath = location.pathname.startsWith("/preview/"); // Check if it's a preview route
|
||||
|
||||
if (isPreviewPath) {
|
||||
@@ -118,60 +120,58 @@ export default function SpellbookPage() {
|
||||
// Navigating to / (Home) will restore the user's dashboard.
|
||||
switchToTemporary(parsedSpellbook);
|
||||
}
|
||||
|
||||
setHasLoadedSpellbook(true); // Mark as loaded, regardless of preview or direct load
|
||||
|
||||
setHasLoadedSpellbook(true); // Mark as loaded, regardless of preview or direct load
|
||||
} catch (e) {
|
||||
console.error("Failed to parse spellbook:", e);
|
||||
toast.error("Failed to load spellbook");
|
||||
setHasLoadedSpellbook(true); // Ensure we don't re-attempt on error
|
||||
}
|
||||
}
|
||||
}, [spellbookEvent, hasLoadedSpellbook, switchToTemporary, applyTemporaryToPersistent, location.pathname]);
|
||||
}, [
|
||||
spellbookEvent,
|
||||
hasLoadedSpellbook,
|
||||
switchToTemporary,
|
||||
applyTemporaryToPersistent,
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
// Cleanup when leaving the page (unmounting)
|
||||
// But wait, if we navigate to /, we want to discard.
|
||||
// If we apply, we navigate to / but we applied first.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// If we are unmounting and still temporary, check if we need to cleanup?
|
||||
// Actually, AppShell wraps this. If we navigate to /, DashboardPage mounts.
|
||||
// DashboardPage doesn't enforce cleanup.
|
||||
// So we should cleanup here if we leave this route without applying.
|
||||
|
||||
// Ideally, we'd check if we are navigating to "Apply".
|
||||
// But applyTemporaryToPersistent clears temporary state internally?
|
||||
// No, it just merges it.
|
||||
|
||||
// Let's look at `useGrimoire`:
|
||||
// applyTemporaryToPersistent -> dispatch({ type: "APPLY_TEMP" }) -> sets grimoireStateAtom = temp, internalTemporaryStateAtom = null.
|
||||
|
||||
// So if we applied, isTemporary is false.
|
||||
// If we navigate away without applying, isTemporary is true.
|
||||
// But we can't easily check "isTemporary" in cleanup function because of closure staleness?
|
||||
// Use a ref or rely on the next component to not show temporary state?
|
||||
// Actually, the global state holds the temporary state.
|
||||
// If the user clicks "Home", they expect their old state.
|
||||
|
||||
// The previous logic in Home.tsx was:
|
||||
// useEffect(() => { if (!actor && isTemporary) discardTemporary() }, [actor, isTemporary])
|
||||
|
||||
// Since we are unmounting SpellbookPage, we are going somewhere else.
|
||||
// If that somewhere else is NOT a spellbook page, we might want to discard.
|
||||
// But maybe we want to keep it if we navigate to "Settings" (modal) or something?
|
||||
// But those are likely overlays.
|
||||
|
||||
// For now, let's rely on the user explicitly discarding or applying via the banner,
|
||||
// OR implement the "Guard" in DashboardPage to discard if it finds itself in temporary mode?
|
||||
// Or just discard on unmount if we didn't apply?
|
||||
// That's hard to track.
|
||||
|
||||
// Let's implement the cleanup in DashboardPage!
|
||||
// If DashboardPage mounts and isTemporary is true, it means we navigated back home.
|
||||
// But wait, what if we "Applied"? Then isTemporary is false.
|
||||
// So if DashboardPage mounts and isTemporary is TRUE, we should discard?
|
||||
// Yes, that replicates the Home.tsx logic: "if (!actor) ... discard".
|
||||
};
|
||||
return () => {
|
||||
// If we are unmounting and still temporary, check if we need to cleanup?
|
||||
// Actually, AppShell wraps this. If we navigate to /, DashboardPage mounts.
|
||||
// DashboardPage doesn't enforce cleanup.
|
||||
// So we should cleanup here if we leave this route without applying.
|
||||
// Ideally, we'd check if we are navigating to "Apply".
|
||||
// But applyTemporaryToPersistent clears temporary state internally?
|
||||
// No, it just merges it.
|
||||
// Let's look at `useGrimoire`:
|
||||
// applyTemporaryToPersistent -> dispatch({ type: "APPLY_TEMP" }) -> sets grimoireStateAtom = temp, internalTemporaryStateAtom = null.
|
||||
// So if we applied, isTemporary is false.
|
||||
// If we navigate away without applying, isTemporary is true.
|
||||
// But we can't easily check "isTemporary" in cleanup function because of closure staleness?
|
||||
// Use a ref or rely on the next component to not show temporary state?
|
||||
// Actually, the global state holds the temporary state.
|
||||
// If the user clicks "Home", they expect their old state.
|
||||
// The previous logic in Home.tsx was:
|
||||
// useEffect(() => { if (!actor && isTemporary) discardTemporary() }, [actor, isTemporary])
|
||||
// Since we are unmounting SpellbookPage, we are going somewhere else.
|
||||
// If that somewhere else is NOT a spellbook page, we might want to discard.
|
||||
// But maybe we want to keep it if we navigate to "Settings" (modal) or something?
|
||||
// But those are likely overlays.
|
||||
// For now, let's rely on the user explicitly discarding or applying via the banner,
|
||||
// OR implement the "Guard" in DashboardPage to discard if it finds itself in temporary mode?
|
||||
// Or just discard on unmount if we didn't apply?
|
||||
// That's hard to track.
|
||||
// Let's implement the cleanup in DashboardPage!
|
||||
// If DashboardPage mounts and isTemporary is true, it means we navigated back home.
|
||||
// But wait, what if we "Applied"? Then isTemporary is false.
|
||||
// So if DashboardPage mounts and isTemporary is TRUE, we should discard?
|
||||
// Yes, that replicates the Home.tsx logic: "if (!actor) ... discard".
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleApplySpellbook = () => {
|
||||
@@ -191,7 +191,7 @@ export default function SpellbookPage() {
|
||||
navigator.clipboard.writeText(link);
|
||||
toast.success("Link copied to clipboard");
|
||||
};
|
||||
|
||||
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const now = Date.now();
|
||||
@@ -213,68 +213,68 @@ export default function SpellbookPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative">
|
||||
{/* Banner Layer */}
|
||||
{showBanner && (
|
||||
<div className="absolute top-0 left-0 right-0 bg-accent text-accent-foreground px-4 py-1.5 flex items-center justify-between text-sm font-medium animate-in slide-in-from-top duration-300 shadow-md z-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<BookHeart className="size-4 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-semibold">
|
||||
{spellbookEvent?.tags.find((t) => t[0] === "title")?.[1] ||
|
||||
"Spellbook"}
|
||||
{/* Banner Layer */}
|
||||
{showBanner && (
|
||||
<div className="absolute top-0 left-0 right-0 bg-accent text-accent-foreground px-4 py-1.5 flex items-center justify-between text-sm font-medium animate-in slide-in-from-top duration-300 shadow-md z-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<BookHeart className="size-4 flex-shrink-0" />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-semibold">
|
||||
{spellbookEvent?.tags.find((t) => t[0] === "title")?.[1] ||
|
||||
"Spellbook"}
|
||||
</span>
|
||||
{spellbookEvent && (
|
||||
<span className="text-xs text-accent-foreground/70 flex items-center gap-2">
|
||||
{authorProfile?.name || resolvedPubkey?.slice(0, 8)}
|
||||
<span className="text-accent-foreground/50">•</span>
|
||||
{formatTimestamp(spellbookEvent.created_at)}
|
||||
</span>
|
||||
{spellbookEvent && (
|
||||
<span className="text-xs text-accent-foreground/70 flex items-center gap-2">
|
||||
{authorProfile?.name || resolvedPubkey?.slice(0, 8)}
|
||||
<span className="text-accent-foreground/50">•</span>
|
||||
{formatTimestamp(spellbookEvent.created_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 hover:bg-black/10 text-accent-foreground"
|
||||
onClick={handleCopyLink}
|
||||
title="Copy share link"
|
||||
>
|
||||
<LinkIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 hover:bg-black/10 text-accent-foreground font-bold"
|
||||
onClick={handleDiscardPreview}
|
||||
>
|
||||
<X className="size-3.5 mr-1" />
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 bg-white text-accent hover:bg-white/90 font-bold shadow-sm"
|
||||
onClick={handleApplySpellbook}
|
||||
>
|
||||
<Check className="size-3.5 mr-1" />
|
||||
Apply Spellbook
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 hover:bg-black/10 text-accent-foreground"
|
||||
onClick={handleCopyLink}
|
||||
title="Copy share link"
|
||||
>
|
||||
<LinkIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 hover:bg-black/10 text-accent-foreground font-bold"
|
||||
onClick={handleDiscardPreview}
|
||||
>
|
||||
<X className="size-3.5 mr-1" />
|
||||
Discard
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 bg-white text-accent hover:bg-white/90 font-bold shadow-sm"
|
||||
onClick={handleApplySpellbook}
|
||||
>
|
||||
<Check className="size-3.5 mr-1" />
|
||||
Apply Spellbook
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading States */}
|
||||
{isResolving && (
|
||||
<div className="absolute top-0 left-0 right-0 z-40 bg-muted px-4 py-2 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>Resolving {actor}...</span>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 right-0 z-40 bg-muted px-4 py-2 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>Resolving {actor}...</span>
|
||||
</div>
|
||||
)}
|
||||
{resolutionError && (
|
||||
<div className="absolute top-0 left-0 right-0 z-40 bg-destructive/10 text-destructive px-4 py-2 flex items-center justify-center text-sm">
|
||||
<span>Failed to resolve actor: {resolutionError}</span>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 right-0 z-40 bg-destructive/10 text-destructive px-4 py-2 flex items-center justify-center text-sm">
|
||||
<span>Failed to resolve actor: {resolutionError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -283,7 +283,9 @@ export default function SpellbookPage() {
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground animate-in fade-in duration-500">
|
||||
<Loader2 className="size-8 animate-spin text-primary/50" />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<p className="font-medium text-foreground">Loading Spellbook...</p>
|
||||
<p className="font-medium text-foreground">
|
||||
Loading Spellbook...
|
||||
</p>
|
||||
<p className="text-xs">Fetching from the relays</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,4 +32,4 @@ const router = createBrowserRouter([
|
||||
|
||||
export default function Root() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user