/* Chat view — messages, composer, hover actions */
const renderText = (text, onMention) => {
if (!text) return null;
const parts = [];
const regex = /(@\w+|#[\w-]+|https?:\/\/\S+|`[^`]+`)/g;
let last = 0;
text.replace(regex, (m, _, idx) => {
if (idx > last) parts.push(text.slice(last, idx));
if (m.startsWith('@')) parts.push( onMention?.(m)}>{m});
else if (m.startsWith('#')) parts.push({m});
else if (m.startsWith('http')) parts.push({m});
else if (m.startsWith('`')) parts.push({m.slice(1, -1)});
last = idx + m.length;
return m;
});
if (last < text.length) parts.push(text.slice(last));
return parts;
};
const Message = ({ msg, onOpenProfile, onReact, onOpenActions, onOpenEmoji }) => {
const hasAvatar = !msg.continuation;
return (
{hasAvatar ? (
onOpenProfile?.(msg.author, e)}>
{msg.avatar}
) : (
{msg.time}
)}
{hasAvatar && (
onOpenProfile?.(msg.author, e)}>{msg.author}
{msg.time}
)}
{msg.text &&
{renderText(msg.text)}
}
{msg.image && (
{msg.image}
)}
{msg.reactions && msg.reactions.length > 0 && (
{msg.reactions.map((r, i) => (
))}
)}
);
};
const ChatHeader = ({ room, roomsData, onToggleMembers, membersOpen, onOpenSearch, onToggleRail, railCollapsed }) => {
const roomDef = findRoom(roomsData, room);
const isDM = roomDef?.type === 'dm';
const title = roomDef?.name || room;
const topic = isDM
? `direct message with ${roomDef.name}`
: 'hi-fi corner for paintings, zines, riso, and whatever else';
return (
{isDM ? (
{roomDef.avatar}
) : (
{roomIcon(roomDef?.type || 'text')}
)}
{title}
{!isDM &&
{topic}
}
{isDM &&
}
{isDM && (
<>
>
)}
);
};
const findRoom = (roomsData, id) => {
if (!roomsData) return null;
for (const section of roomsData.sections) {
const r = section.rooms.find(r => r.id === id);
if (r) return r;
}
return null;
};
const Composer = ({ room, onSend, onOpenEmoji }) => {
const [text, setText] = React.useState('');
const ref = React.useRef(null);
const submit = () => {
if (text.trim()) {
onSend(text);
setText('');
}
};
const handleKey = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
};
const roomName = room?.name || room;
const placeholder = typeof roomName === 'string' && roomName.startsWith('dm-')
? `Message…`
: `Message #${typeof roomName === 'string' ? roomName.replace('dm-', '') : 'room'}`;
return (
);
};
const MembersPanel = ({ room, members }) => {
const data = members || { online: [], offline: [] };
return (
);
};
const DateDivider = ({ label }) => {label}
;
const ChatView = ({ room, roomsData, messages, members, onOpenProfile, onOpenActions, onOpenEmoji, onToggleMembers, membersOpen, onOpenSearch, onToggleRail, railCollapsed, onSend, onReact }) => {
const messagesRef = React.useRef(null);
React.useEffect(() => {
if (messagesRef.current) messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}, [room, messages?.length]);
const roomDef = findRoom(roomsData, room);
return (
{messages?.map(m => (
))}
);
};
Object.assign(window, { ChatView, Message, Composer, ChatHeader, MembersPanel, DateDivider, findRoom, roomIcon });