import { useEffect, useRef, useState } from 'react' import { useWaste } from '../store' const today = new Date() today.setHours(0, 0, 0, 0) const todayMs = today.getTime() function formatTs(ts: number): string { const d = new Date(ts) const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) if (ts >= todayMs) return time return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + time } const URL_RE = /https?:\/\/[^\s<>"']+/g const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|svg)(\?[^\s]*)?$/i function renderText(text: string): React.ReactNode { const parts: React.ReactNode[] = [] let last = 0 let m: RegExpExecArray | null URL_RE.lastIndex = 0 while ((m = URL_RE.exec(text)) !== null) { if (m.index > last) parts.push(text.slice(last, m.index)) const url = m[0] const isImage = IMAGE_EXT_RE.test(url) || url.startsWith('blob:') || url.startsWith('data:image') parts.push( {url} ) if (isImage) { parts.push( ) } last = m.index + url.length } if (last < text.length) parts.push(text.slice(last)) return parts.length > 1 ? <>{parts} : text } const EMOJI_SET = ['👍', '❤️', '😂', '😮', '😢', '🙏'] export function MessagePane({ onMenuClick }: { onMenuClick: () => void }) { const { messages, historyCutoff, activeRoom, activeNetworkId, localPeer, connectedPeers, knownPeers, reactions, sendReaction, send } = useWaste() const [draft, setDraft] = useState('') const [pickerMid, setPickerMid] = useState(null) const bottomRef = useRef(null) const msgKey = activeNetworkId ? `${activeNetworkId}:${activeRoom}` : activeRoom const roomMessages = messages[msgKey] ?? [] const cutoff = historyCutoff[msgKey] ?? 0 const firstLiveIdx = cutoff > 0 ? roomMessages.findIndex(m => m.ts > cutoff) : -1 const dividerIdx = cutoff > 0 ? (firstLiveIdx === -1 ? 0 : firstLiveIdx) : -1 useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [roomMessages.length]) // Close picker when clicking outside useEffect(() => { if (!pickerMid) return const handler = () => setPickerMid(null) document.addEventListener('click', handler) return () => document.removeEventListener('click', handler) }, [pickerMid]) function submit(e: React.FormEvent) { e.preventDefault() const text = draft.trim() if (!text || !activeNetworkId) return if (activeRoom.startsWith('dm:')) { const toPeerID = activeRoom.slice(3) const peer = connectedPeers.find(p => p.id.startsWith(toPeerID) || p.id === toPeerID) if (peer) { send({ type: 'send_message', network_id: activeNetworkId, room: activeRoom, body: text, to: peer.id }) } } else { send({ type: 'send_message', network_id: activeNetworkId, room: activeRoom, body: text }) } setDraft('') } function aliasFor(fromId: string) { if (fromId === localPeer?.id) return localPeer.alias return connectedPeers.find(p => p.id === fromId)?.alias ?? knownPeers[fromId] ?? fromId.slice(0, 8) } function toggleReaction(mid: string, emoji: string, e: React.MouseEvent) { e.stopPropagation() if (!activeNetworkId || !mid) return sendReaction(activeNetworkId, mid, emoji) setPickerMid(null) } function openPicker(mid: string, e: React.MouseEvent) { e.stopPropagation() setPickerMid(prev => prev === mid ? null : mid) } const roomLabel = activeRoom.startsWith('dm:') ? `@ ${activeRoom.slice(3, 11)}…` : `# ${activeRoom}` return (
{roomLabel}
{dividerIdx === 0 && (
earlier messages
)} {roomMessages.map((msg, i) => { const mine = msg.from === localPeer?.id const alias = aliasFor(String(msg.from)) const ts = formatTs(msg.ts) const mid = msg.mid ?? '' const msgReactions = mid ? reactions[mid] : undefined const hasReactions = msgReactions && Object.keys(msgReactions).length > 0 return (
{i === dividerIdx && dividerIdx > 0 && (
earlier messages
)}
{ts} {alias} {renderText(msg.text)} {mid && ( )}
{pickerMid === mid && (
e.stopPropagation()}> {EMOJI_SET.map(emoji => ( ))}
)} {hasReactions && (
{Object.entries(msgReactions!).map(([emoji, fromIds]) => { const iMine = fromIds.includes(localPeer?.id ?? '') return ( ) })}
)}
) })}
setDraft(e.target.value)} placeholder={`Message ${roomLabel}`} autoComplete="off" />
) }