// Csoport chat — projekt-szintű üzenetváltás, file attach, "Kérdés" funkció (email triggerrel) const Chat = ({ user, onClose, onToast }) => { const [, force] = React.useReducer(x => x + 1, 0); const [text, setText] = React.useState(''); const [attachments, setAttachments] = React.useState([]); const [askUser, setAskUser] = React.useState(null); const [showAskPicker, setShowAskPicker] = React.useState(false); const fileRef = React.useRef(null); const scrollRef = React.useRef(null); const [reactionPickerFor, setReactionPickerFor] = React.useState(null); const REACTION_SET = [ { emoji: '👍', label: 'Like' }, { emoji: '👎', label: 'Dislike' }, { emoji: '😄', label: 'Happy' }, { emoji: '❤️', label: 'Love' }, { emoji: '😢', label: 'Cry' }, { emoji: '🫡', label: 'Salute' }, { emoji: '🙏', label: 'Thank' }, { emoji: '🚀', label: 'Rocket' }, ]; const toggleReaction = async (msg, emoji) => { // Optimistic local toggle so the UI feels instant. if (!msg.reactions) msg.reactions = {}; const list = msg.reactions[emoji] || []; const idx = list.indexOf(user.id); if (idx >= 0) { list.splice(idx, 1); if (list.length === 0) delete msg.reactions[emoji]; else msg.reactions[emoji] = list; } else { msg.reactions[emoji] = [...list, user.id]; } setReactionPickerFor(null); force(); try { await window.api.messages.reaction({ message_id: msg.id, emoji }); await window.refreshData(); } catch (err) { onToast(window.apiErrorMessage(err)); } }; // Auto-scroll a végére React.useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [window.MESSAGES.length]); // Olvasott jelölés: amikor megnyílik, kérjük a szerveren is, hogy // jegyezze meg a jelenlegi időpontot (törli az unread badge-et). React.useEffect(() => { window.CHAT_LAST_READ = new Date().toISOString(); (async () => { try { await window.api.messages.read(); } catch (e) { /* offline ok */ } })(); return () => { window.CHAT_LAST_READ = new Date().toISOString(); }; }, []); const projectMembers = window.USERS; const sendMessage = async () => { if (!text.trim() && attachments.length === 0) return; const body = text.trim(); const askedId = askUser ? askUser.id : null; const askedEmail = askUser?.email; const files = attachments.map(a => a._file).filter(Boolean); // Optimistic local push so the bubble shows up immediately. const optimistic = { id: (window.MESSAGES.length ? Math.max(...window.MESSAGES.map(m => m.id)) : 0) + 1000000, authorId: user.id, body, createdAt: new Date().toISOString(), askedUserId: askedId, attachments: attachments.map(a => ({ name: a.name, type: a.type, size: a.size, url: a.url })), reactions: {}, _pending: true, }; window.MESSAGES.push(optimistic); setText(''); setAttachments([]); setAskUser(null); force(); try { const fd = new FormData(); fd.append('body', body); if (askedId) fd.append('asked_user_id', String(askedId)); files.forEach(f => fd.append('attachments[]', f)); await window.api.messages.create(fd); // Refresh from server so the optimistic placeholder is replaced with the // canonical record (with proper id, server timestamps, attachment URLs). await window.refreshData(); if (askedEmail) onToast(`✉ Email küldve: ${askedEmail}`); } catch (err) { // Roll back the optimistic message. const i = window.MESSAGES.findIndex(m => m.id === optimistic.id); if (i >= 0) window.MESSAGES.splice(i, 1); force(); onToast('Hiba: ' + window.apiErrorMessage(err)); } }; const onFilePick = (e) => { const files = Array.from(e.target.files || []); const next = files.map(f => ({ _file: f, name: f.name, type: f.type.startsWith('image/') ? 'image' : 'file', size: (f.size / 1024 / 1024).toFixed(1) + ' MB', url: f.type.startsWith('image/') ? URL.createObjectURL(f) : null, })); setAttachments([...attachments, ...next]); e.target.value = ''; }; const removeAttachment = (i) => { setAttachments(attachments.filter((_, idx) => idx !== i)); }; // Csoportosítás nap szerint const grouped = []; let lastDay = null; window.MESSAGES.forEach(m => { const d = m.createdAt.slice(0, 10); if (d !== lastDay) { grouped.push({ kind: 'day', day: d }); lastDay = d; } grouped.push({ kind: 'msg', msg: m }); }); return (
{/* Desktop sidebar — projekt tagok */}
{/* Header */}
H1 csapat
{projectMembers.length} tag
{projectMembers.slice(0, 4).map((u, i) => (
))} {projectMembers.length > 4 && (
+{projectMembers.length - 4}
)}
{/* Üzenetek */}
{grouped.map((g, i) => { if (g.kind === 'day') { return (
{window.relativeDay(g.day)}
); } const m = g.msg; const author = window.getUser(m.authorId); const mine = m.authorId === user.id; const askedUser = m.askedUserId ? window.getUser(m.askedUserId) : null; const time = m.createdAt.slice(11, 16); return (
{!mine && }
{!mine && (
{author?.name?.split(' ')[0]} · {time}
)} {askedUser && (
Kérdés {askedUser.name.split(' ')[0]} felé · email küldve
)}
{m.body &&
{m.body}
} {m.attachments && m.attachments.length > 0 && (
{m.attachments.map((a, j) => (
{a.type === 'image' && a.url ? ( {a.name} ) : a.type === 'image' ? (
{a.name}
) : (
{a.name}
{a.size}
)}
))}
)} {mine && (
{time}
)}
{/* Reactions */}
{m.reactions && Object.entries(m.reactions).map(([emoji, userIds]) => { const reactedByMe = userIds.includes(user.id); const names = userIds.map(uid => window.getUser(uid)?.name?.split(' ')[0]).filter(Boolean).join(', '); return ( ); })} {reactionPickerFor === m.id && (
{REACTION_SET.map(r => ( ))}
)}
); })}
{/* Composer */}
{/* Pending kérdés címke */} {askUser && (
Kérdés {askUser.name} felé · email értesítés
)} {/* Pending csatolmányok */} {attachments.length > 0 && (
{attachments.map((a, i) => (
{a.type === 'image' && a.url ? ( ) : ( )} {a.name}
))}
)}