90 lines · 3.6 KB
import { useState } from "react";
import { toggleReaction } from "~/fns/reactions";
import { useOptimisticMutation } from "~/hooks/use-optimistic-mutation";
import { REACTION_CONTENTS, REACTION_EMOJI } from "~/lib/reactions";
import type { ReactionContent, ReactionSubject, ReactionSummary } from "~/lib/reactions";
import { cn } from "~/utils/cn";
import { useDismiss } from "~/hooks/use-dismiss";

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] : []));
}

/** 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 toggle = useOptimisticMutation(reactions, toggleLocally, (content: ReactionContent) =>
    toggleReaction({ data: { owner, repo, subjectType, subjectId, content } }),
  );
  const ref = useDismiss<HTMLDivElement>(open, () => setOpen(false));
  const react = (content: ReactionContent) => {
    setOpen(false);
    toggle.run(content);
  };
  if (!canReact && reactions.length === 0) return null;
  return (
    <div ref={ref} className="relative flex flex-wrap items-center gap-1.5">