import React, { useState, useRef, useEffect, useMemo } from 'react'; import { network, ADMIN_PUBLIC_KEY } from '../p2p/index.js'; import ServerInviteCard from './ServerInviteCard.jsx'; import UserProfileModal from './UserProfileModal.jsx'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkBreaks from 'remark-breaks'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; const QUICK_EMOJIS = ['โค๏ธ', '๐Ÿ˜‚', '๐Ÿ’ฏ', '๐Ÿ”ฅ']; const ALL_EMOJIS = ['๐Ÿ˜€','๐Ÿ˜‚','๐Ÿคฃ','๐Ÿ˜Š','๐Ÿ˜','๐Ÿฅฐ','๐Ÿ˜Ž','๐Ÿคฉ','๐Ÿ˜˜','๐Ÿ˜—','๐Ÿ˜‹','๐Ÿ˜›','๐Ÿ˜œ','๐Ÿคช','๐Ÿ˜','๐Ÿค‘','๐Ÿค—','๐Ÿคญ','๐Ÿคซ','๐Ÿค”','๐Ÿค','๐Ÿคจ','๐Ÿ˜','๐Ÿ˜‘','๐Ÿ˜ถ','๐Ÿ˜','๐Ÿ˜’','๐Ÿ™„','๐Ÿ˜ฌ','๐Ÿคฅ','๐Ÿ˜Œ','๐Ÿ˜”','๐Ÿ˜ช','๐Ÿคค','๐Ÿ˜ด','๐Ÿ˜ท','๐Ÿค’','๐Ÿค•','๐Ÿคข','๐Ÿคฎ','๐Ÿคง','๐Ÿฅต','๐Ÿฅถ','๐Ÿฅด','๐Ÿ˜ต','๐Ÿคฏ','๐Ÿค ','๐Ÿฅณ','๐Ÿ˜Ž','๐Ÿค“','๐Ÿง','๐Ÿ˜•','๐Ÿ˜Ÿ','๐Ÿ™','โ˜น๏ธ','๐Ÿ˜ฎ','๐Ÿ˜ฏ','๐Ÿ˜ฒ','๐Ÿ˜ณ','๐Ÿฅบ','๐Ÿ˜ฆ','๐Ÿ˜ง','๐Ÿ˜จ','๐Ÿ˜ฐ','๐Ÿ˜ฅ','๐Ÿ˜ข','๐Ÿ˜ญ','๐Ÿ˜ฑ','๐Ÿ˜–','๐Ÿ˜ฃ','๐Ÿ˜ž','๐Ÿ˜“','๐Ÿ˜ฉ','๐Ÿ˜ซ','๐Ÿฅฑ','๐Ÿ˜ค','๐Ÿ˜ก','๐Ÿ˜ ','๐Ÿคฌ','๐Ÿ˜ˆ','๐Ÿ‘ฟ','๐Ÿ’€','โ˜ ๏ธ','๐Ÿ’ฉ','๐Ÿคก','๐Ÿ‘น','๐Ÿ‘บ','๐Ÿ‘ป','๐Ÿ‘ฝ','๐Ÿ‘พ','๐Ÿค–','๐Ÿ˜บ','๐Ÿ˜ธ','๐Ÿ˜น','๐Ÿ˜ป','๐Ÿ˜ผ','๐Ÿ˜ฝ','๐Ÿ™€','๐Ÿ˜ฟ','๐Ÿ˜พ','โค๏ธ','๐Ÿงก','๐Ÿ’›','๐Ÿ’š','๐Ÿ’™','๐Ÿ’œ','๐Ÿ–ค','๐Ÿค','๐ŸคŽ','๐Ÿ’”','โฃ๏ธ','๐Ÿ’•','๐Ÿ’ž','๐Ÿ’“','๐Ÿ’—','๐Ÿ’–','๐Ÿ’˜','๐Ÿ’','๐Ÿ‘','๐Ÿ‘Ž','๐Ÿ‘Š','โœŠ','๐Ÿค›','๐Ÿคœ','๐Ÿคž','โœŒ๏ธ','๐ŸคŸ','๐Ÿค˜','๐Ÿ‘Œ','๐Ÿค','๐Ÿ‘ˆ','๐Ÿ‘‰','๐Ÿ‘†','๐Ÿ‘‡','โ˜๏ธ','โœ‹','๐Ÿคš','๐Ÿ–','๐Ÿ––','๐Ÿ‘‹','๐Ÿค™','๐Ÿ’ช','๐Ÿฆพ','๐Ÿ–•','โœ๏ธ','๐Ÿ™','๐Ÿฆถ','๐Ÿฆต','๐Ÿฆฟ','๐Ÿ’„','๐Ÿ’‹','๐Ÿ‘„','๐Ÿฆท','๐Ÿ‘…','๐Ÿ‘‚','๐Ÿฆป','๐Ÿ‘ƒ','๐Ÿ‘ฃ','๐Ÿ‘','๐Ÿ‘€','๐Ÿง ','๐Ÿ—ฃ','๐Ÿ‘ค','๐Ÿ‘ฅ','๐Ÿ’ฏ','๐Ÿ’ข','๐Ÿ’ฅ','๐Ÿ’ซ','๐Ÿ’ฆ','๐Ÿ’จ','๐Ÿ•ณ','๐Ÿ’ฃ','๐Ÿ’ฌ','๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ','๐Ÿ—จ๏ธ','๐Ÿ—ฏ๏ธ','๐Ÿ’ญ','๐Ÿ’ค']; const MarkdownComponents = { code({node, inline, className, children, ...props}) { const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( {String(children).replace(/\n$/, '')} ) : ( {children} ) }, a: ({node, href, children, ...props}) => { if (href && href.startsWith('mention://')) { return {children}; } if (href && href.startsWith('channel://')) { return {children}; } return {children}; }, p: ({node, children, ...props}) =>

{children}

}; const processMentionsAndChannels = (text) => { if (!text) return ''; let processed = text.replace(/(^|\s)(@everyone|@[a-zA-Z0-9_.]+)/g, '$1[**$2**](mention://$2)'); processed = processed.replace(/(^|\s)(#[a-z0-9-]+)/g, '$1[**$2**](channel://$2)'); return processed; }; const formatBytes = (bytes, decimals = 2) => { if (!+bytes) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes =['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; }; const DECRYPTION_INITIAL_DELAY_MS = 400; const DECRYPTION_SPEED_MS = 30; const DECRYPTION_CHARS_PER_TICK = 1; const DecryptedMessage = ({ msg, liveDecryption, animationTrigger, components }) => { const [isAnimating, setIsAnimating] = useState(false); const [revealed, setRevealed] = useState(0); const [gibberish, setGibberish] = useState(''); const lastTrigger = useRef(animationTrigger); useEffect(() => { if (!liveDecryption || !msg.isEncrypted || !msg.text) { setIsAnimating(false); return; } const isNewTrigger = animationTrigger !== lastTrigger.current; lastTrigger.current = animationTrigger; const cacheKey = `animated_${msg.id}`; if (!isNewTrigger && sessionStorage.getItem(cacheKey)) { setIsAnimating(false); return; } sessionStorage.setItem(cacheKey, 'true'); const text = msg.text; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; let gib = ''; let seed = 0; const cipher = msg.cipher || 'fallback'; for(let i=0; i { interval = setInterval(() => { curr += DECRYPTION_CHARS_PER_TICK; if (curr >= text.length) { setRevealed(text.length); setIsAnimating(false); clearInterval(interval); } else { setRevealed(curr); } }, DECRYPTION_SPEED_MS); }, DECRYPTION_INITIAL_DELAY_MS); return () => { clearTimeout(timeout); if (interval) clearInterval(interval); }; }, [msg.text, msg.cipher, msg.isEncrypted, liveDecryption, msg.id, animationTrigger]); if (!isAnimating) { return (
value} > {processMentionsAndChannels(msg.text || '')}
); } return (
{msg.text.substring(0, revealed)} {gibberish.substring(revealed)}
); }; export default function ChatArea({ activeView, activeChannel, setActiveChannel, messages, myKey, profile, typingUsers, readReceipts, deliveredReceipts, onlinePeers, markChannelRead, dms, servers, onStartCall, activeCall, onReturnToCall, transfers, onOpenInvite, onToggleMembers, pinMembers, onNavigateToDM }) { const[inputText, setInputText] = useState(''); const[editingId, setEditingId] = useState(null); const[editInput, setEditInput] = useState(''); const[activeTypers, setActiveTypers] = useState([]); const[replyingTo, setReplyingTo] = useState(null); const [attachments, setAttachments] = useState([]); const[isDragging, setIsDragging] = useState(false); const[expandedImage, setExpandedImage] = useState(null); const [contextMenu, setContextMenu] = useState(null); const [fullEmojiPicker, setFullEmojiPicker] = useState(null); const [emojiPickerDirection, setEmojiPickerDirection] = useState('down'); const [profileViewUser, setProfileViewUser] = useState(null); const [mentionType, setMentionType] = useState(null); // 'user' | 'channel' const [mentionQuery, setMentionQuery] = useState(null); const [mentionIndex, setMentionIndex] = useState(0); const [filteredMentions, setFilteredMentions] = useState([]); const [showCrypto, setShowCrypto] = useState(false); const [liveDecryption, setLiveDecryption] = useState(localStorage.getItem('pear_live_decryption') === 'true'); const [ircMode, setIrcMode] = useState(localStorage.getItem('pear_irc_mode') === 'true'); const [animationTrigger, setAnimationTrigger] = useState(0); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const textareaRef = useRef(null); const editTextareaRef = useRef(null); const fileInputRef = useRef(null); const lastTypingTime = useRef(0); const lastSentReadIdRef = useRef(null); const isDMView = activeView === 'dms'; const gcObj = isDMView ? servers.find(s => s.topicHex === activeChannel && s.isGroupChat) : null; const isGroupChat = !!gcObj; const networkChannelId = isGroupChat ? activeChannel : (isDMView ? activeChannel : `${activeView}-${activeChannel}`); let isAdmin = myKey === ADMIN_PUBLIC_KEY; let canPost = true; let canUpload = true; let canReact = true; if (!isDMView || isGroupChat) { const activeServerObj = servers.find(s => s.topicHex === (isGroupChat ? activeChannel : activeView)); if (activeServerObj) { if (activeServerObj.owner === myKey) { isAdmin = true; } if (!isAdmin) { const userRoles = activeServerObj.memberRoles?.[myKey] || []; const isServerAdmin = userRoles.some(rId => { const r = activeServerObj.roles?.find(role => role.id === rId); return r && r.permissions.includes('admin'); }); if (!isServerAdmin) { const channelPerms = activeServerObj.channels?.permissions?.[activeChannel]; if (channelPerms && channelPerms.length > 0) { const hasChannelAccess = userRoles.some(rId => channelPerms.includes(rId)); if (!hasChannelAccess) { canPost = false; canUpload = false; canReact = false; } } const channelSendPerms = activeServerObj.channels?.send_permissions?.[activeChannel]; if (channelSendPerms && channelSendPerms.length > 0) { const hasChannelSendAccess = userRoles.some(rId => channelSendPerms.includes(rId)); if (!hasChannelSendAccess) { canPost = false; canUpload = false; } } if (canPost) { const hasSendPerm = userRoles.some(rId => { const r = activeServerObj.roles?.find(role => role.id === rId); return r && r.permissions.includes('send_messages'); }); if (!hasSendPerm && activeServerObj.roles && activeServerObj.roles.length > 0) canPost = false; } if (canUpload) { const hasFilePerm = userRoles.some(rId => { const r = activeServerObj.roles?.find(role => role.id === rId); return r && r.permissions.includes('send_files'); }); if (!hasFilePerm && activeServerObj.roles && activeServerObj.roles.length > 0) canUpload = false; } if (canReact) { const hasReactPerm = userRoles.some(rId => { const r = activeServerObj.roles?.find(role => role.id === rId); return r && r.permissions.includes('add_reactions'); }); if (!hasReactPerm && activeServerObj.roles && activeServerObj.roles.length > 0) canReact = false; } } } } } const openProfile = (pubKey) => { const user = network.knownProfiles.get(pubKey) || { key: pubKey, displayName: 'Unknown User', username: 'unknown' }; setProfileViewUser({ key: pubKey, ...user }); }; const markdownComponents = useMemo(() => ({ code({node, inline, className, children, ...props}) { const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( {String(children).replace(/\n$/, '')} ) : ( {children} ) }, a: ({node, href, children, ...props}) => { if (href && href.startsWith('mention://')) { return ( { e.preventDefault(); e.stopPropagation(); const username = href.replace('mention://@', ''); if (username === 'everyone') return; let foundPubKey = null; for (const [key, profile] of network.knownProfiles.entries()) { if (profile.username === username) { foundPubKey = key; break; } } if (foundPubKey) { openProfile(foundPubKey); } else { const dirUser = network.userDirectory?.get(username); if (dirUser && dirUser.pubKey) openProfile(dirUser.pubKey); else openProfile(username); } }} className="bg-[#5865F2]/30 !text-[#c9cdfb] hover:bg-[#5865F2] hover:!text-white px-1.5 py-0.5 rounded-md font-medium cursor-pointer transition-colors no-underline" > {children} ); } if (href && href.startsWith('channel://')) { return ( { e.preventDefault(); e.stopPropagation(); const channelName = href.replace('channel://#', ''); if (setActiveChannel) setActiveChannel(channelName); }} className="bg-[#5865F2]/30 !text-[#c9cdfb] hover:bg-[#5865F2] hover:!text-white px-1.5 py-0.5 rounded-md font-medium cursor-pointer transition-colors no-underline" > {children} ); } return {children}; }, p: ({node, children, ...props}) =>

{children}

}), [setActiveChannel]); useEffect(() => { const handleStorage = () => { setIrcMode(localStorage.getItem('pear_irc_mode') === 'true'); setLiveDecryption(localStorage.getItem('pear_live_decryption') === 'true'); }; window.addEventListener('storage', handleStorage); return () => window.removeEventListener('storage', handleStorage); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "auto" }); setReplyingTo(null); setMentionQuery(null); setMentionType(null); },[activeChannel, activeView]); useEffect(() => { const handleClick = () => { setContextMenu(null); setFullEmojiPicker(null); }; if (contextMenu || fullEmojiPicker) document.addEventListener('click', handleClick); return () => document.removeEventListener('click', handleClick); }, [contextMenu, fullEmojiPicker]); useEffect(() => { const handleJump = (e) => { const msgId = e.detail; const el = document.getElementById(`msg-${msgId}`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.classList.add('bg-accent/20'); setTimeout(() => el.classList.remove('bg-accent/20'), 2000); } }; window.addEventListener('jump-to-message', handleJump); return () => window.removeEventListener('jump-to-message', handleJump); }, []); useEffect(() => { if (chatContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current; const isNearBottom = scrollHeight - scrollTop - clientHeight < 200; if (isNearBottom) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } } const sendRead = () => { const currentChannelMessages = messages.filter(m => { if (isDMView && !isGroupChat) return (m.sender === myKey && m.recipient === activeChannel) || (m.sender === activeChannel && m.recipient === myKey); return m.channel === networkChannelId && !m.recipient; }); const latestMsg = currentChannelMessages[currentChannelMessages.length - 1]; const latestMsgId = latestMsg ? latestMsg.id : null; if (latestMsgId && latestMsgId !== lastSentReadIdRef.current) { network.sendReadReceipt(networkChannelId, latestMsgId); lastSentReadIdRef.current = latestMsgId; } markChannelRead(networkChannelId); }; sendRead(); const interval = setInterval(sendRead, 3000); return () => clearInterval(interval); // eslint-disable-next-line react-hooks/exhaustive-deps },[messages.length, activeChannel, isDMView, isGroupChat, myKey, onlinePeers.length, activeView]); useEffect(() => { const interval = setInterval(() => { const now = Date.now(); const typers = Object.entries(typingUsers) .filter(([key, info]) => { if (now - info.timestamp >= 3000 || key === myKey) return false; return (isDMView && !isGroupChat) ? (key === activeChannel && info.channel === myKey) : (info.channel === networkChannelId); }) .map(([_, info]) => info.displayName); setActiveTypers(typers); }, 1000); return () => clearInterval(interval); },[typingUsers, activeChannel, myKey, isDMView, isGroupChat, networkChannelId]); useEffect(() => { if (editingId && editTextareaRef.current) { editTextareaRef.current.style.height = 'auto'; editTextareaRef.current.style.height = `${Math.min(editTextareaRef.current.scrollHeight, 400)}px`; editTextareaRef.current.focus(); } },[editingId]); useEffect(() => { if (mentionQuery !== null) { if (mentionType === 'user') { let users = []; if (isDMView && !isGroupChat) { const dmUser = dms[activeChannel]?.profile || { displayName: 'Unknown', username: 'unknown' }; users = [dmUser]; } else { const serverObj = servers.find(s => s.topicHex === (isGroupChat ? activeChannel : activeView)); if (serverObj) { const members = network.serverMembers[serverObj.topicHex] ? Array.from(network.serverMembers[serverObj.topicHex]) : []; if (!members.includes(serverObj.owner)) members.push(serverObj.owner); users = members.map(k => network.knownProfiles.get(k)).filter(Boolean); } } const query = mentionQuery.toLowerCase(); const filtered = users.filter(u => u.username.toLowerCase().includes(query) || u.displayName.toLowerCase().includes(query)); let canMentionEveryone = isAdmin; if (!isAdmin && !isDMView) { const serverObj = servers.find(s => s.topicHex === activeView); if (serverObj) { const userRoles = serverObj.memberRoles?.[myKey] || []; canMentionEveryone = userRoles.some(rId => { const r = serverObj.roles?.find(role => role.id === rId); return r && (r.permissions.includes('admin') || r.permissions.includes('mention_everyone')); }); } } if ('everyone'.includes(query) && (canMentionEveryone || isDMView)) { filtered.unshift({ username: 'everyone', displayName: 'Everyone in this channel', avatar: null, isSpecial: true }); } setFilteredMentions(filtered); } else if (mentionType === 'channel') { const serverObj = servers.find(s => s.topicHex === activeView); if (serverObj && serverObj.channels) { const allChannels = [ ...(serverObj.channels.text || []).map(ch => ({ isChannel: true, name: ch, type: 'text' })), ...(serverObj.channels.voice || []).map(ch => ({ isChannel: true, name: ch, type: 'voice' })) ]; const query = mentionQuery.toLowerCase(); const filtered = allChannels.filter(ch => ch.name.toLowerCase().includes(query)); setFilteredMentions(filtered); } else { setFilteredMentions([]); } } setMentionIndex(0); } }, [mentionQuery, mentionType, activeView, activeChannel, isDMView, isGroupChat, dms, servers, isAdmin, myKey]); const processFiles = async (files) => { const newAttachments =[]; for (let i = 0; i < files.length; i++) { const file = files[i]; newAttachments.push({ name: file.name, size: file.size, type: file.type, path: file.path, fileObj: file, preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : null }); } setAttachments(prev =>[...prev, ...newAttachments]); }; const handleDrop = (e) => { e.preventDefault(); setIsDragging(false); if (canUpload && e.dataTransfer.files && e.dataTransfer.files.length > 0) { processFiles(e.dataTransfer.files); } }; const handlePaste = (e) => { if (canUpload && e.clipboardData.files && e.clipboardData.files.length > 0) { processFiles(e.clipboardData.files); } }; const handleInputChange = (e) => { const val = e.target.value; setInputText(val); if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 400)}px`; } const cursor = e.target.selectionStart; const textBeforeCursor = val.slice(0, cursor); const userMatch = textBeforeCursor.match(/(^|\s)@([a-zA-Z0-9_.]*)$/); const channelMatch = textBeforeCursor.match(/(^|\s)#([a-z0-9-]*)$/); if (userMatch) { setMentionType('user'); setMentionQuery(userMatch[2]); } else if (channelMatch && !isDMView) { setMentionType('channel'); setMentionQuery(channelMatch[2]); } else { setMentionType(null); setMentionQuery(null); } const now = Date.now(); if (now - lastTypingTime.current > 2000) { network.sendTyping(networkChannelId); lastTypingTime.current = now; } }; const insertMention = (item) => { if (!item) return; const cursor = textareaRef.current.selectionStart; const textBeforeCursor = inputText.slice(0, cursor); const textAfterCursor = inputText.slice(cursor); if (mentionType === 'user') { const match = textBeforeCursor.match(/(^|\s)@([a-zA-Z0-9_.]*)$/); if (match) { const newTextBefore = textBeforeCursor.slice(0, match.index) + match[1] + `@${item.username} `; setInputText(newTextBefore + textAfterCursor); setMentionQuery(null); setMentionType(null); setTimeout(() => { if (textareaRef.current) { textareaRef.current.focus(); textareaRef.current.selectionStart = newTextBefore.length; textareaRef.current.selectionEnd = newTextBefore.length; } }, 0); } } else if (mentionType === 'channel') { const match = textBeforeCursor.match(/(^|\s)#([a-z0-9-]*)$/); if (match) { const newTextBefore = textBeforeCursor.slice(0, match.index) + match[1] + `#${item.name} `; setInputText(newTextBefore + textAfterCursor); setMentionQuery(null); setMentionType(null); setTimeout(() => { if (textareaRef.current) { textareaRef.current.focus(); textareaRef.current.selectionStart = newTextBefore.length; textareaRef.current.selectionEnd = newTextBefore.length; } }, 0); } } }; const handleKeyDown = (e) => { if (mentionQuery !== null && filteredMentions.length > 0) { if (e.key === 'ArrowDown') { e.preventDefault(); setMentionIndex(prev => (prev + 1) % filteredMentions.length); return; } if (e.key === 'ArrowUp') { e.preventDefault(); setMentionIndex(prev => (prev - 1 + filteredMentions.length) % filteredMentions.length); return; } if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); insertMention(filteredMentions[mentionIndex]); return; } if (e.key === 'Escape') { setMentionQuery(null); setMentionType(null); return; } } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); const replyId = replyingTo ? replyingTo.id : null; if (attachments.length > 0) { for (let i = 0; i < attachments.length; i++) { const textToSend = i === 0 ? inputText.trim() : ''; if (isDMView && !isGroupChat) network.sendDMFile(activeChannel, textToSend, attachments[i]); else network.sendFile(networkChannelId, textToSend, attachments[i]); } setAttachments([]); setInputText(''); setReplyingTo(null); if (textareaRef.current) textareaRef.current.style.height = 'auto'; } else if (inputText.trim() !== '') { if (isDMView && !isGroupChat) network.sendDM(activeChannel, inputText.trim(), replyId); else network.sendMessage(networkChannelId, inputText.trim(), replyId); setInputText(''); setReplyingTo(null); if (textareaRef.current) textareaRef.current.style.height = 'auto'; } } }; const startEditing = (msg) => { setEditingId(msg.id); setEditInput(msg.text); }; const handleEditChange = (e) => { setEditInput(e.target.value); if (editTextareaRef.current) { editTextareaRef.current.style.height = 'auto'; editTextareaRef.current.style.height = `${Math.min(editTextareaRef.current.scrollHeight, 400)}px`; } }; const handleEditMessage = (e, id) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (editInput.trim() !== '') network.sendEditMessage(id, editInput.trim()); setEditingId(null); } else if (e.key === 'Escape') { setEditingId(null); } }; const handleOpenFolder = async (filePath) => { if (!filePath) return; try { if (typeof Pear !== 'undefined') { const { spawn } = await import('child_process'); const os = await import('os'); const path = await import('path'); const platform = os.platform(); if (platform === 'win32') { const child = spawn('explorer.exe',['/select,', filePath], { detached: true }); child.unref(); } else if (platform === 'darwin') { const child = spawn('open',['-R', filePath], { detached: true }); child.unref(); } else { const dir = path.dirname(filePath); const child = spawn('xdg-open',[dir], { detached: true }); child.unref(); } } else if (typeof window !== 'undefined' && window.require) { const { shell } = window.require('electron'); shell.showItemInFolder(filePath); } } catch (err) { console.error("Failed to open folder:", err.message || err); } }; const handleCopyText = (text) => { if (text) navigator.clipboard.writeText(text); }; const handleCopyImage = async (url) => { try { const response = await fetch(url); const blob = await response.blob(); await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); } catch (err) { console.error("Failed to copy image", err); } }; const handleToggleLiveDecryption = () => { const newVal = !liveDecryption; setLiveDecryption(newVal); localStorage.setItem('pear_live_decryption', newVal); if (newVal) { setAnimationTrigger(Date.now()); } }; const currentChannelMessages = messages.filter(m => { if (isDMView && !isGroupChat) return (m.sender === myKey && m.recipient === activeChannel) || (m.sender === activeChannel && m.recipient === myKey); return m.channel === networkChannelId && !m.recipient; }); const myMessages = currentChannelMessages.filter(m => m.sender === myKey); const lastMyMessageId = myMessages.length > 0 ? myMessages[myMessages.length - 1].id : null; const readMsgId = readReceipts[networkChannelId]; const explicitDeliveredMsgId = deliveredReceipts ? deliveredReceipts[networkChannelId] : null; const readMsgIndex = currentChannelMessages.findIndex(m => m.id === readMsgId); const explicitDeliveredMsgIndex = currentChannelMessages.findIndex(m => m.id === explicitDeliveredMsgId); const isPeerOnline = (isDMView && !isGroupChat) ? onlinePeers.some(p => p.key === activeChannel) : onlinePeers.length > 0; let effectiveDeliveredMsgIndex = explicitDeliveredMsgIndex; if (isPeerOnline && isDMView && !isGroupChat) effectiveDeliveredMsgIndex = currentChannelMessages.length - 1; let lastMyReadMsgId = null; let lastMyDeliveredMsgId = null; for (let i = currentChannelMessages.length - 1; i >= 0; i--) { const m = currentChannelMessages[i]; if (m.sender === myKey) { if (!lastMyDeliveredMsgId && effectiveDeliveredMsgIndex !== -1 && i <= effectiveDeliveredMsgIndex) lastMyDeliveredMsgId = m.id; if (!lastMyReadMsgId && readMsgIndex !== -1 && i <= readMsgIndex) lastMyReadMsgId = m.id; } } const getMessageStatus = (msg) => { if (msg.sender !== myKey) return null; const msgIndex = currentChannelMessages.findIndex(m => m.id === msg.id); if (isDMView && !isGroupChat) { if (msg.id === lastMyReadMsgId) { const hasNewerReply = currentChannelMessages.slice(msgIndex + 1).some(m => m.sender !== myKey); if (!hasNewerReply) { const targetProfile = dms[activeChannel]?.profile || {}; return (
{targetProfile.avatar ? ( ) : (
{targetProfile.displayName?.substring(0, 2).toUpperCase() || '?'}
)}
); } } const isAfterRead = readMsgIndex === -1 || msgIndex > readMsgIndex; if (isAfterRead) { const isDelivered = effectiveDeliveredMsgIndex !== -1 && msgIndex <= effectiveDeliveredMsgIndex; if (isDelivered) return โœ“โœ“; else return โœ“; } return null; } else { if (msg.id !== lastMyMessageId) return null; const isRead = readMsgIndex !== -1 && msgIndex <= readMsgIndex; if (isRead) return โœ“โœ“; if (isPeerOnline) return โœ“โœ“; return โœ“; } }; const headerName = isGroupChat ? gcObj.name : (isDMView ? (dms[activeChannel]?.profile?.displayName || 'Unknown') : activeChannel); const headerIcon = isGroupChat ? '๐Ÿ‘ฅ' : (isDMView ? '@' : '#'); let typingText = ''; if (activeTypers.length === 1) typingText = `${activeTypers[0]} is typing...`; else if (activeTypers.length > 1) typingText = `Several people are typing...`; const isCallActiveInThisDM = activeCall && activeCall.targetKey === activeChannel; return (
{ e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={handleDrop} className="flex-1 flex flex-col bg-panel min-w-0 relative h-full" > {isDragging && canUpload && (

Drop files to upload

)} {expandedImage && (
setExpandedImage(null)}> e.stopPropagation()} />
)} {profileViewUser && ( setProfileViewUser(null)} onSendDM={profileViewUser.key !== myKey ? (u) => { if (!dms[u.key]) { network.sendDMRequest(u.key, { displayName: u.displayName, username: u.username, avatar: u.avatar, bio: u.bio, connections: u.connections }); } if (onNavigateToDM) onNavigateToDM(u.key); } : null} /> )} {contextMenu && (
setContextMenu(null)} onContextMenu={(e) => { e.preventDefault(); setContextMenu(null); }}>
e.stopPropagation()} > {canReact && (
{QUICK_EMOJIS.map(emoji => ( ))}
)} {canPost && ( )} {contextMenu.msg.text && ( )} {contextMenu.msg.payload?.type === 'file' && contextMenu.msg.payload.file.mimeType?.startsWith('image/') && ( )} {contextMenu.isMe && contextMenu.msg.payload?.type !== 'server_invite' && ( )} {(contextMenu.isAdmin || contextMenu.isMe) && ( )}
)}
{headerIcon} {headerName}
{isDMView && !isGroupChat && ( <> )} {(isDMView || isGroupChat) && ( <> {isGroupChat && ( )} {isCallActiveInThisDM ? ( ) : ( <> )} )} {(!isDMView || isGroupChat) && !pinMembers && ( )}
{headerIcon}

{isGroupChat ? `Welcome to ${headerName}!` : (isDMView ? headerName : `Welcome to #${headerName}!`)}

{isGroupChat ? `This is the beginning of your group whisper history.` : (isDMView ? `This is the beginning of your whisper history with ${headerName}.` : `This is the start of the decentralized #${headerName} room.`)}

{currentChannelMessages.map((msg) => { const isPlatformAdmin = msg.sender === ADMIN_PUBLIC_KEY; let isServerOwner = false; let canDelete = false; if (!isDMView || isGroupChat) { const activeServerObj = servers.find(s => s.topicHex === (isGroupChat ? activeChannel : activeView)); if (activeServerObj) { if (activeServerObj.owner === msg.sender) isServerOwner = true; if (activeServerObj.owner === myKey) canDelete = true; else { const userRoles = activeServerObj.memberRoles?.[myKey] || []; const hasAdmin = userRoles.some(rId => { const r = activeServerObj.roles?.find(role => role.id === rId); return r && r.permissions.includes('admin'); }); if (hasAdmin) canDelete = true; } } } const showCrown = isPlatformAdmin || isServerOwner; const crownTitle = isServerOwner ? (isGroupChat ? "Group Creator" : "Hub Owner") : "Platform Admin"; const isMe = msg.sender === myKey; if (ircMode) { return (
{ e.preventDefault(); setContextMenu({ x: e.pageX, y: e.pageY, msg, isMe, isAdmin: canDelete || isPlatformAdmin }); }} > [{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}] openProfile(msg.sender)}> <{isMe ? `${profile.displayName}` : msg.senderName}>
{msg.replyTo && (() => { const repliedMsg = messages.find(m => m.id === msg.replyTo); if (repliedMsg) return document.getElementById(`msg-${repliedMsg.id}`)?.scrollIntoView({behavior: 'smooth'})}>@{repliedMsg.senderName}; return null; })()} {msg.payload?.type === 'file' ? ( [File: {msg.payload.file.name}] ) : msg.payload?.type === 'server_invite' ? ( [Server Invite: {msg.payload.serverName}] ) : ( {msg.text} )} {msg.edited && (edited)}
{/* Actions for IRC Mode */}
{canReact && QUICK_EMOJIS.map(emoji => ( ))} {canReact && (
)} {canPost && ( )} {isMe && msg.payload?.type !== 'server_invite' && ( )} {(canDelete || isPlatformAdmin || isMe) && ( )}
{fullEmojiPicker === msg.id && (
e.stopPropagation()}> {ALL_EMOJIS.map(emoji => ( ))}
)}
); } return (
{ e.preventDefault(); setContextMenu({ x: e.pageX, y: e.pageY, msg, isMe, isAdmin: canDelete || isPlatformAdmin }); }} > {/* Reply Row */} {msg.replyTo && (() => { const repliedMsg = messages.find(m => m.id === msg.replyTo); if (!repliedMsg) return
Replying to a deleted message
; return (
document.getElementById(`msg-${repliedMsg.id}`)?.scrollIntoView({behavior: 'smooth'})} >
{repliedMsg.senderAvatar ? ( ) : (
{repliedMsg.senderName.substring(0, 2).toUpperCase()}
)} @{repliedMsg.senderName} {repliedMsg.text || 'Attachment'}
); })()} {/* Main Message Row */}
openProfile(msg.sender)} > {msg.senderAvatar ? : msg.senderName.substring(0, 2).toUpperCase()}
openProfile(msg.sender)}> {isMe ? `${profile.displayName} (You)` : msg.senderName} {showCrown && ๐Ÿ‘‘} {new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {getMessageStatus(msg)}
{showCrypto && msg.isEncrypted ? (
Algorithm: xchacha20poly1305_ietf
Nonce: {msg.nonce}
{msg.cipher}
) : editingId === msg.id ? (