Files
llink/js/desktop/src/features/particles/reaction-bar.tsx
T

218 lines
7.5 KiB
TypeScript

import { useState } from 'react';
import { Plus, Type, X } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { HumanAvatar } from '@/components/human-avatar';
import { REACTION_EMOJIS, type Reactions, type Human } from '@/api/types';
import { cn } from '@/lib/utils';
import { resolveHumanDisplay } from '@/lib/humans';
interface ReactionBarProps {
reactions: Reactions;
currentHumanId: string;
humans?: Human[];
onToggle: (key: string) => void;
onOpenTextReaction: () => void;
}
const EMOJI_SET = new Set<string>(REACTION_EMOJIS);
function getReactorNames(humanIds: string[], humans?: Human[]): string {
return humanIds
.map((id) => resolveHumanDisplay(id, humans).displayName)
.join(', ');
}
function getReactorList(
humanIds: string[],
humans: Human[] | undefined,
currentHumanId: string,
): { id: string; label: string; isMine: boolean }[] {
return humanIds.map((id) => ({
id,
label: resolveHumanDisplay(id, humans).displayName,
isMine: id === currentHumanId,
}));
}
export function ReactionBar({
reactions,
currentHumanId,
humans,
onToggle,
onOpenTextReaction,
}: ReactionBarProps) {
const [expanded, setExpanded] = useState(false);
const activeEmojis = REACTION_EMOJIS.filter(
(emoji) => reactions?.[emoji] && reactions[emoji].length > 0,
);
const activeTextReactions = Object.keys(reactions ?? {}).filter(
(key) => !EMOJI_SET.has(key) && (reactions?.[key]?.length ?? 0) > 0,
);
const handleToggle = (key: string) => {
onToggle(key);
setExpanded(false);
};
return (
<div className="flex flex-col items-end gap-1.5">
{/* Emoji reaction pills */}
{activeEmojis.map((emoji) => {
const reactors = reactions?.[emoji] ?? [];
const isMine = reactors.includes(currentHumanId);
return (
<Tooltip key={emoji}>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
handleToggle(emoji);
}}
className={cn(
'flex items-center gap-1 rounded-full px-2 py-0.5 text-xs backdrop-blur-sm transition-colors',
isMine
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
)}
>
<span className="text-sm">{emoji}</span>
<span className="text-muted-foreground">{reactors.length}</span>
</button>
</TooltipTrigger>
<TooltipContent side="left" className="text-xs">
{getReactorNames(reactors, humans)}
</TooltipContent>
</Tooltip>
);
})}
{/* Text reaction pills */}
{activeTextReactions.map((text) => {
const reactors = reactions?.[text] ?? [];
const isMine = reactors.includes(currentHumanId);
const firstReactor = resolveHumanDisplay(reactors[0], humans);
const reactorList = getReactorList(reactors, humans, currentHumanId);
return (
<Tooltip key={text}>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
handleToggle(text);
}}
className={cn(
'flex max-w-[200px] items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2.5 text-xs backdrop-blur-sm transition-colors',
isMine
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
)}
>
<HumanAvatar
size="xs"
className="shrink-0"
avatarObjectId={firstReactor.avatarObjectId}
initials={firstReactor.initials}
fallbackClassName="bg-muted text-muted-foreground text-[9px] font-medium"
/>
<span className="truncate text-foreground">{text}</span>
{reactors.length > 1 && (
<span className="text-muted-foreground shrink-0">
{reactors.length}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent
side="left"
className="max-w-[260px] space-y-1.5 text-xs"
>
<div className="font-medium">{text}</div>
<ul className="flex flex-col gap-0.5 opacity-80">
{reactorList.map((r) => (
<li
key={r.id}
className={cn(r.isMine && 'font-medium opacity-100')}
>
{r.label}
{r.isMine && <span className="ml-1 opacity-60">(you)</span>}
</li>
))}
</ul>
<div className="border-t border-current/15 pt-1 text-[10px] opacity-60">
{isMine ? 'Click to remove' : 'Click to add yours'}
</div>
</TooltipContent>
</Tooltip>
);
})}
{/* Picker / actions */}
{expanded ? (
<div className="bg-card/80 border-border flex flex-col items-center gap-0.5 rounded-full border px-0.5 py-1.5 backdrop-blur-sm">
{REACTION_EMOJIS.map((emoji) => {
if (activeEmojis.includes(emoji)) return null;
return (
<button
key={emoji}
onClick={(e) => {
e.stopPropagation();
handleToggle(emoji);
}}
className="hover:bg-accent rounded-full px-0.5 py-1 text-sm transition-colors"
>
{emoji}
</button>
);
})}
<button
onClick={(e) => {
e.stopPropagation();
setExpanded(false);
}}
className="hover:bg-accent flex size-5 items-center justify-center rounded-full transition-colors"
>
<X className="text-muted-foreground size-3" />
</button>
</div>
) : (
<div className="flex flex-col items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
onOpenTextReaction();
}}
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
>
<Type className="text-muted-foreground size-3" />
</button>
</TooltipTrigger>
<TooltipContent side="left" className="text-xs">
Quick reply{' '}
<kbd className="bg-muted ml-1 rounded px-1 font-mono text-[10px]">
R
</kbd>
</TooltipContent>
</Tooltip>
<button
onClick={(e) => {
e.stopPropagation();
setExpanded(true);
}}
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
>
<Plus className="text-muted-foreground size-3" />
</button>
</div>
)}
</div>
);
}