import {
type ComponentType,
createContext,
type MouseEvent,
type ReactNode,
type Ref,
type SVGProps,
useContext,
} from 'react';
import { clsx } from 'clsx';
import { formatPostStatCount } from '#/locale/intl/number';
import { AnimatedLikeIcon } from '#/components/PostControls/AnimatedLikeIcon';
import { CountWheel } from '#/components/PostControls/CountWheel';
import { Text } from '#/components/Text';
import { Tooltip } from '#/components/Tooltip';
import * as Skele from '#/components/web/Skeleton';
import ArrowShareRightIcon from '#/icons/central/ArrowShareRight_round_outlined_radius1_stroke2.svg';
import Repost from '#/icons/central/ArrowsRepeatRightLeft_round_outlined_radius1_stroke2.svg';
import Bubble from '#/icons/central/Bubble2_round_outlined_radius1_stroke2.svg';
import { m } from '#/paraglide/messages';
import { colors } from '#/styles/colors';
import * as css from './index.css';
import { RepostMenu } from './RepostMenu';
import { type PostControlsProps, usePostControlsActions } from './shared';
import { ShareMenu } from './ShareMenu';
const PostControlContext = createContext<{ active?: boolean }>({});
PostControlContext.displayName = 'PostControlContext';
type PostControlButtonProps = {
/** Accessible name; becomes the `aria-label`. */
label: string;
/** Visible hover/focus hint. Pass `null` to suppress it (e.g. when a wrapping menu owns the tooltip). */
tooltip: string | null;
children: ReactNode;
active?: boolean;
/** Color applied when `active`; icon + count inherit it via `currentColor`. */
activeColor?: string;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
ref?: Ref<HTMLButtonElement>;
};
/**
* a compact post-control action button that renders as a plain button. can render standalone or back a
* Menu.Trigger.
*/
function PostControlButton({
active,
activeColor,
children,
label,
onClick,
tooltip,
...rest
}: PostControlButtonProps) {
const button = (
// Base UI's `Menu.Trigger render={...}` and `Tooltip` both clone this with their own props
// (aria/data/handlers/id/ref) merged in, so spread them all onto the button — forwarding only the
// ref wouldn't open the menu.
<button
type="button"
aria-label={label}
className={css.button}
style={active && activeColor ? { color: activeColor } : undefined}
onClick={onClick}
{...rest}
>
<PostControlContext.Provider value={{ active }}>{children}</PostControlContext.Provider>
</button>
);
if (tooltip === null) {
return button;
}
return <Tooltip label={tooltip}>{button}</Tooltip>;
}
/**
* wraps a control's icon in a hover-highlighted circle so the hover/focus chrome lands on the icon alone. use
* directly for a custom icon; see {@link PostControlButtonIcon} for a plain icon.
*/
function PostControlButtonIconBox({ children }: { children: ReactNode }) {
return <span className={css.iconCircle}>{children}</span>;
}
/** A plain icon in the hover circle, inheriting the button color via `currentColor`. */
function PostControlButtonIcon({ icon: Icon }: { icon: ComponentType<SVGProps<SVGSVGElement>> }) {
return (
<PostControlButtonIconBox>
<Icon className={css.icon} />
</PostControlButtonIconBox>
);
}
/** A count/label beside the icon, inheriting the button color and bolding when active. */
function PostControlButtonText({ children }: { children: ReactNode }) {
const { active } = useContext(PostControlContext);
return (
<Text
className={css.text}
leading="none"
selectable={false}
size="md_sub"
weight={active ? 'semiBold' : undefined}
>
{children}
</Text>
);
}
/** The compact post action bar used on the feed and thread rows. */
export function PostControls({
post,
feedContext,
reqId,
onPressReply,
onPostReply,
viaRepost,
}: PostControlsProps): ReactNode {
const {
hasLikeIconBeenToggled,
onPressToggleLike,
onQuote,
onRepost,
onShare,
replyDisabled,
requireAuth,
} = usePostControlsActions({ post, feedContext, reqId, viaRepost, onPostReply });
const repostCount = (post.repostCount ?? 0) + (post.quoteCount ?? 0);
return (
<div className={css.root}>
<div className={css.primaryGroup}>
<div className={clsx(css.primaryItem, replyDisabled && css.replyDisabled)}>
<PostControlButton
onClick={
!replyDisabled
? () =>
requireAuth(() => {
onPressReply();
})
: undefined
}
label={m['components.postControls.reply.a11y']({ count: post.replyCount || 0 })}
tooltip={m['common.action.reply']()}
>
<PostControlButtonIcon icon={Bubble} />
{typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
<PostControlButtonText>{formatPostStatCount(post.replyCount)}</PostControlButtonText>
)}
</PostControlButton>
</div>
<div className={css.primaryItem}>
<RepostMenu
isReposted={!!post.viewer?.repost}
onRepost={() => void onRepost()}
onQuote={onQuote}
embeddingDisabled={!!post.viewer?.embeddingDisabled}
tooltip={m['components.postControls.repost.action.repost']()}
render={
<PostControlButton
label={m['components.postControls.repost.a11y']()}
tooltip={null}
active={!!post.viewer?.repost}
activeColor={colors.positive_500}
>
<PostControlButtonIcon icon={Repost} />
{repostCount > 0 && (
<PostControlButtonText>{formatPostStatCount(repostCount)}</PostControlButtonText>
)}
</PostControlButton>
}
/>
</div>
<div className={css.primaryItem}>
<PostControlButton
active={!!post.viewer?.like}
activeColor={colors.pink}
tooltip={m['common.action.like']()}
onClick={() => requireAuth(() => onPressToggleLike())}
label={
post.viewer?.like
? m['components.postControls.like.a11y.unlike']({ count: post.likeCount || 0 })
: m['components.postControls.like.a11y.like']({ count: post.likeCount || 0 })
}
>
<PostControlButtonIconBox>
<AnimatedLikeIcon isLiked={!!post.viewer?.like} hasBeenToggled={hasLikeIconBeenToggled} />
</PostControlButtonIconBox>
<CountWheel
count={post.likeCount ?? 0}
isToggled={!!post.viewer?.like}
hasBeenToggled={hasLikeIconBeenToggled}
renderCount={({ count }) => (
<PostControlButtonText>{formatPostStatCount(count)}</PostControlButtonText>
)}
/>
</PostControlButton>
</div>
</div>
<div className={css.secondaryGroup}>
<ShareMenu
post={post}
onShare={onShare}
tooltip={m['common.share.action.share']()}
render={
<PostControlButton label={m['components.postControls.share.a11y']()} tooltip={null}>
<PostControlButtonIcon icon={ArrowShareRightIcon} />
</PostControlButton>
}
/>
</div>
</div>
);
}
export function PostControlsSkeleton() {
// the rest-state row shows the bare icons, so the bars stand in at `ICON_SIZE` — the live `iconCircle` is
// only the hover target and pulls itself back to that footprint with a negative margin. the trailing share
// control reuses `secondaryGroup` verbatim so it pins to the same edge as the live row.
return (
<div className={css.root}>
<div className={css.primaryGroup}>
<div className={css.primaryItem}>
<Skele.Pill blend size={css.ICON_SIZE} />
</div>
<div className={css.primaryItem}>
<Skele.Pill blend size={css.ICON_SIZE} />
</div>
<div className={css.primaryItem}>
<Skele.Pill blend size={css.ICON_SIZE} />
</div>
</div>
<div className={css.secondaryGroup}>
<Skele.Circle blend size={css.ICON_SIZE} />
</div>
</div>
);
}