import React, { useState, useEffect, useRef } from 'react'; import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'; import browser from 'webextension-polyfill'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; type CommentReactions = { [emoji: string]: string[] }; interface ReactionsProps { issueId: number; commentId: string; } async function getAnonymousUserId(): Promise { const data = await browser.storage.local.get('userId'); if (data.userId) return data.userId; const newUserId = crypto.randomUUID(); await browser.storage.local.set({ userId: newUserId }); return newUserId; } const Reactions: React.FC = ({ issueId, commentId }) => { const [reactions, setReactions] = useState({}); const [myUserId, setMyUserId] = useState(null); const [isLoading, setIsLoading] = useState(true); const [showPicker, setShowPicker] = useState(false); const pickerContainerRef = useRef(null); useEffect(() => { const fetchData = async () => { try { const userId = await getAnonymousUserId(); setMyUserId(userId); const response = await fetch(`${API_BASE_URL}/api/reactions/${issueId}`); if (!response.ok) throw new Error('Network response was not ok'); const data = await response.json(); if (data && data[commentId]) { setReactions(data[commentId]); } } catch (error) { console.error("Failed to fetch reactions:", error); } finally { setIsLoading(false); } }; fetchData(); const handleClickOutside = (event: MouseEvent) => { if (pickerContainerRef.current && !pickerContainerRef.current.contains(event.target as Node)) { setShowPicker(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [issueId, commentId]); const handleReactionClick = async (emoji: string) => { if (!myUserId) return; const currentReactions = reactions[emoji] || []; const hasReacted = currentReactions.includes(myUserId); const method = hasReacted ? 'DELETE' : 'POST'; const previousReactions = { ...reactions }; setReactions(prev => { const newUsers = hasReacted ? (prev[emoji] || []).filter(id => id !== myUserId) : [...(prev[emoji] || []), myUserId]; const newReactionsForComment = { ...prev, [emoji]: newUsers }; if (newReactionsForComment[emoji].length === 0) delete newReactionsForComment[emoji]; return newReactionsForComment; }); try { const response = await fetch(`${API_BASE_URL}/api/reactions`, { method, headers: { 'Content-Type': 'application/json', 'X-User-ID': myUserId }, body: JSON.stringify({ issueId, commentId, emoji }), }); if (!response.ok) throw new Error('Server returned an error'); } catch (error) { console.error("Failed to update reaction:", error); setReactions(previousReactions); } }; const onEmojiClick = (emojiData: EmojiClickData) => { setShowPicker(false); handleReactionClick(emojiData.emoji); }; if (isLoading || !myUserId) return null; return (
{Object.entries(reactions).map(([emoji, users]) => { if (users.length === 0) return null; const isSelected = users.includes(myUserId); return (
handleReactionClick(emoji)}> {emoji} {users.length}
); })}
{showPicker && (
)}
); }; const styles: { [key: string]: React.CSSProperties } = { container: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px', marginTop: '8px', marginBottom: '8px', paddingLeft: '10px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', fontSize: '13px' }, badge: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px solid #dcdcdc', borderRadius: '12px', cursor: 'pointer', transition: 'background-color 0.2s', backgroundColor: '#f7f7f7' }, badgeSelected: { backgroundColor: '#e6f2ff', borderColor: '#99ccff' }, emoji: { marginRight: '4px', fontSize: '15px', lineHeight: '1' }, count: { fontWeight: 600, color: '#333' }, countSelected: { color: '#005cc5' }, addButton: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px dashed #ccc', borderRadius: '12px', backgroundColor: 'transparent', cursor: 'pointer', color: '#555', fontFamily: 'inherit', fontSize: '13px' }, pickerWrapper: { position: 'absolute', bottom: '100%', left: 0, marginBottom: '10px', zIndex: 1000 }, }; export default Reactions;