gitdataai/src/components/room/RoomMessageReactions.tsx
2026-04-15 09:08:09 +08:00

126 lines
3.8 KiB
TypeScript

import { useState, useCallback, useEffect } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { SmilePlus } from 'lucide-react';
import { client } from '@/client/client.gen';
interface ReactionGroup {
emoji: string;
count: number;
reacted_by_me: boolean;
users: string[];
}
interface RoomMessageReactionsProps {
roomId: string;
messageId: string;
className?: string;
}
const COMMON_EMOJIS = [
'👍', '👎', '❤️', '😂', '😮', '😢', '🎉', '🚀',
'✅', '⭐', '🔥', '💯', '👀', '🙏', '💪', '🤔',
];
export function RoomMessageReactions({
roomId,
messageId,
className,
}: RoomMessageReactionsProps) {
const [reactions, setReactions] = useState<ReactionGroup[]>([]);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
loadReactions();
}, [messageId]);
const loadReactions = useCallback(async () => {
try {
const resp = await client.get({
url: `/api/rooms/${roomId}/messages/${messageId}/reactions`,
});
const data = (resp.data as any)?.data as { reactions: ReactionGroup[] } | undefined;
if (data) {
setReactions(data.reactions);
}
} catch (err) {
console.error('Failed to load reactions:', err);
}
}, [roomId, messageId]);
const handleReaction = useCallback(async (emoji: string) => {
try {
const existingReaction = reactions.find(r => r.emoji === emoji);
if (existingReaction?.reacted_by_me) {
await client.delete({
url: `/api/rooms/${roomId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`,
});
} else {
await client.post({
url: `/api/rooms/${roomId}/messages/${messageId}/reactions`,
body: { emoji },
});
}
await loadReactions();
} catch (err) {
console.error('Failed to update reaction:', err);
} finally {
setIsOpen(false);
}
}, [roomId, messageId, reactions, loadReactions]);
// Compact inline reaction bar
return (
<div className={cn('flex items-center gap-0.5', className)}>
{reactions.map((reaction) => (
<button
key={reaction.emoji}
onClick={() => handleReaction(reaction.emoji)}
className={cn(
'inline-flex items-center gap-0.5 rounded-full border px-1.5 py-0.5 text-xs transition-colors',
reaction.reacted_by_me
? 'border-primary/50 bg-primary/10 text-primary'
: 'border-border bg-muted/20 text-muted-foreground hover:bg-muted/40'
)}
title={`${reaction.count} reaction${reaction.count !== 1 ? 's' : ''}`}
>
<span>{reaction.emoji}</span>
<span className="font-medium">{reaction.count}</span>
</button>
))}
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/50 hover:bg-accent hover:text-foreground">
<SmilePlus className="h-3 w-3" />
</PopoverTrigger>
<PopoverContent className="w-64 p-2" align="start">
<EmojiPicker onEmojiSelect={handleReaction} />
</PopoverContent>
</Popover>
</div>
);
}
interface EmojiPickerProps {
onEmojiSelect: (emoji: string) => void;
}
function EmojiPicker({ onEmojiSelect }: EmojiPickerProps) {
return (
<div className="grid grid-cols-8 gap-1">
{COMMON_EMOJIS.map((emoji) => (
<button
key={emoji}
onClick={() => onEmojiSelect(emoji)}
className="flex h-7 w-7 items-center justify-center rounded-md text-base hover:bg-accent transition-colors"
title={emoji}
>
{emoji}
</button>
))}
</div>
);
}