Files
eranos/src/components/PostActionBar.tsx
T
Alex Gleason 3e31d26660 Replace Twitter-style action bar with GoFundMe-style chip row
The campaign page (and every post feed) inherited Ditto's spread-out
pill action bar — icons across the full width, framed by a heavy
top+bottom border band, with cascading dots on the 'Show more replies'
thread connector. It read as a Twitter toolbar, which is wrong for a
fundraising client.

Restyle the shared action row across PostActionBar and NoteCard:
- Chip-style buttons (h-9 px-3 rounded-full) with inline counts.
- Engagement actions cluster left, share/more pushed right with a
  flex spacer instead of justify-between.
- Drop the unconditional top+bottom border band; pages add their own
  separator via className when needed.
- Show 'React'/'Reply'/'Repost' word labels on sm+ when the count is
  zero, so the bar reads as labelled affordances rather than icon
  pills floating in space.
- Add a 'chip' variant to ReactionButton so it can switch between the
  legacy pill (still used by PhotoBottomBar, PodcastDetailContent,
  MusicDetailContent, BookFeedItem, PostDetailPage, etc.) and the new
  chip look.

ThreadedReplyList's 'Show N more replies' button drops the four
cascading dots in favour of a single soft connector that brightens on
hover.

On the campaign page itself, wrap the engagement stats + action bar
in a soft card, add a 'Comments & donations' section heading with a
count, and replace the bare 'No comments yet' line with a dashed
empty-state CTA that opens the reply composer.
2026-05-18 22:47:18 -05:00

163 lines
5.8 KiB
TypeScript

import type { NostrEvent } from '@nostrify/nostrify';
import { MessageCircle, MoreHorizontal, Share2, Zap } from 'lucide-react';
import { nip19 } from 'nostr-tools';
import { useCallback } from 'react';
import { RepostIcon } from '@/components/icons/RepostIcon';
import { ReactionButton } from '@/components/ReactionButton';
import { RepostMenu } from '@/components/RepostMenu';
import { ZapDialog } from '@/components/ZapDialog';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useEventStats } from '@/hooks/useTrending';
import { useToast } from '@/hooks/useToast';
import { canZap } from '@/lib/canZap';
import { formatNumber } from '@/lib/formatNumber';
import { hasGoalZapSplits } from '@/lib/goalUtils';
import { shareOrCopy } from '@/lib/share';
import { cn } from '@/lib/utils';
interface PostActionBarProps {
event: NostrEvent;
/** Label and action for the first (reply/comments) button. */
replyLabel?: string;
onReply: () => void;
onMore: () => void;
/** Hide the zap button entirely. Useful for events with their own donation
* flow (e.g. fundraising campaigns) where a generic Lightning zap is the
* wrong primary CTA. Defaults to false. */
hideZap?: boolean;
/** Extra classes on the outer wrapper div. */
className?: string;
}
export function PostActionBar({
event,
replyLabel = 'Reply',
onReply,
onMore,
hideZap = false,
className,
}: PostActionBarProps) {
const { toast } = useToast();
const { user } = useCurrentUser();
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
// TODO: Enable zapping split-recipient NIP-75 goals once zap split payments are supported.
const canZapAuthor = !hideZap && user && canZap(metadata) && !hasGoalZapSplits(event);
const { data: stats } = useEventStats(event.id, event);
const repostTotal = (stats?.reposts ?? 0) + (stats?.quotes ?? 0);
const handleShare = useCallback(async () => {
let encoded: string;
if (event.kind >= 30000 && event.kind < 40000) {
const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? '';
encoded = nip19.naddrEncode({ kind: event.kind, pubkey: event.pubkey, identifier: dTag });
} else if (event.kind >= 10000 && event.kind < 20000) {
encoded = nip19.naddrEncode({ kind: event.kind, pubkey: event.pubkey, identifier: '' });
} else {
encoded = nip19.neventEncode({ id: event.id, author: event.pubkey });
}
const url = `${window.location.origin}/${encoded}`;
const result = await shareOrCopy(url);
if (result === 'copied') toast({ title: 'Link copied to clipboard' });
}, [event, toast]);
return (
<div
className={cn(
// Soft chip-style action row. Buttons cluster to the left
// (engagement) with share/more pushed right. No heavy
// top/bottom border band — pages can add their own separator
// via `className` if they need one.
'flex flex-wrap items-center gap-1 sm:gap-2',
className,
)}
>
{/* Reply / Comments */}
<button
className="inline-flex items-center gap-2 h-9 px-3 rounded-full text-sm font-medium text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
title={replyLabel}
onClick={onReply}
>
<MessageCircle className="size-[18px]" />
{stats?.replies ? (
<span className="tabular-nums">{formatNumber(stats.replies)}</span>
) : (
<span className="hidden sm:inline">{replyLabel}</span>
)}
</button>
{/* Repost */}
<RepostMenu event={event}>
{(isReposted: boolean) => (
<button
className={cn(
'inline-flex items-center gap-2 h-9 px-3 rounded-full text-sm font-medium transition-colors',
isReposted
? 'text-accent hover:text-accent/80 hover:bg-accent/10'
: 'text-muted-foreground hover:text-accent hover:bg-accent/10',
)}
title={isReposted ? 'Undo repost' : 'Repost'}
>
<RepostIcon className="size-[18px]" />
{repostTotal > 0 ? (
<span className="tabular-nums">{formatNumber(repostTotal)}</span>
) : (
<span className="hidden sm:inline">Repost</span>
)}
</button>
)}
</RepostMenu>
{/* React */}
<ReactionButton
eventId={event.id}
eventPubkey={event.pubkey}
eventKind={event.kind}
reactionCount={stats?.reactions}
variant="chip"
/>
{/* Zap */}
{canZapAuthor && (
<ZapDialog target={event}>
<button
className="inline-flex items-center gap-2 h-9 px-3 rounded-full text-sm font-medium text-muted-foreground hover:text-amber-500 hover:bg-amber-500/10 transition-colors"
title="Zap"
>
<Zap className="size-[18px]" />
{stats?.zapAmount ? (
<span className="tabular-nums">{formatNumber(stats.zapAmount)}</span>
) : (
<span className="hidden sm:inline">Zap</span>
)}
</button>
</ZapDialog>
)}
{/* Spacer pushes share/more to the right */}
<div className="flex-1" />
{/* Share */}
<button
className="inline-flex items-center justify-center h-9 w-9 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors sidebar:hidden"
title="Share"
onClick={handleShare}
>
<Share2 className="size-[18px]" />
</button>
{/* More */}
<button
className="inline-flex items-center justify-center h-9 w-9 rounded-full text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
title="More"
onClick={onMore}
>
<MoreHorizontal className="size-[18px]" />
</button>
</div>
);
}