import { Popover } from "@cloudflare/kumo/components/popover"; import { SELECTED_SURFACE } from "~/ui/tones"; import { useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { reactorsQueryKey, useReactors } from "~/hooks/use-reactors"; import { toggleReaction } from "~/fns/reactions"; import { useOptimisticMutation } from "~/hooks/use-optimistic-mutation"; import { REACTION_CONTENTS, REACTION_EMOJI, reactorsLabel } from "~/lib/reactions"; import type { ReactionContent, ReactionSubject, ReactionSummary } from "~/lib/reactions"; import { cn } from "~/utils/cn"; type Props = { owner: string; repo: string; subjectType: ReactionSubject; subjectId: string; reactions: ReactionSummary[]; canReact: boolean; }; function toggleLocally(current: ReactionSummary[], content: ReactionContent): ReactionSummary[] { const existing = current.find((reaction) => reaction.content === content); if (!existing) return [...current, { content, count: 1, viewerReacted: true }]; const next = existing.viewerReacted ? { ...existing, count: existing.count - 1, viewerReacted: false } : { ...existing, count: existing.count + 1, viewerReacted: true }; return current.flatMap((reaction) => (reaction.content !== content ? [reaction] : next.count > 0 ? [next] : [])); } type PillProps = { target: { owner: string; repo: string; subjectType: ReactionSubject; subjectId: string; content: ReactionContent }; reaction: ReactionSummary; disabled: boolean; onToggle: () => void; }; function ReactionPill({ target, reaction, disabled, onToggle }: PillProps) { const [hovered, setHovered] = useState(false); const reactors = useReactors(target, hovered); const label = reactors.data ? `${reactorsLabel(reactors.data, reaction.count)} reacted with ${REACTION_EMOJI[reaction.content]}` : "Loading…"; return ( setHovered(true)} onMouseLeave={() => setHovered(false)} onFocus={() => setHovered(true)} onBlur={() => setHovered(false)} > {hovered && ( {label} )} ); } /** Reaction pills plus an emoji picker; reusable on issues, PRs, comments, review comments and discussions. */ export function ReactionBar({ owner, repo, subjectType, subjectId, reactions, canReact }: Props) { const [open, setOpen] = useState(false); const queryClient = useQueryClient(); const toggle = useOptimisticMutation(reactions, toggleLocally, async (content: ReactionContent) => { const result = await toggleReaction({ data: { owner, repo, subjectType, subjectId, content } }); await queryClient.invalidateQueries({ queryKey: reactorsQueryKey({ owner, repo, subjectType, subjectId, content }), }); return result; }); const react = (content: ReactionContent) => { setOpen(false); toggle.run(content); }; if (!canReact && reactions.length === 0) return null; return (