[READ-ONLY] Mirror of https://github.com/aaronateataco/ermine. A stoat web client erminechat.vercel.app
0

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #10 from FascinatingPistachio/codex/preload-website-to-prevent-crashes

Preload chat data and improve mention/emoji rendering

authored by

Fascinating Pistachio and committed by
GitHub
(Feb 14, 2026, 1:31 PM UTC) 0d565bd1 547ab892

+92 -5
+92 -5
src/App.jsx
··· 46 46 return `${cdnUrl}/icons/${server.icon._id}`; 47 47 }; 48 48 49 + const EMOJI_SHORTCODES = { 50 + smile: '😄', 51 + grin: '😁', 52 + joy: '😂', 53 + rofl: '🤣', 54 + wink: '😉', 55 + heart: '❤️', 56 + thumbs_up: '👍', 57 + thumbs_down: '👎', 58 + fire: '🔥', 59 + sob: '😭', 60 + thinking: '🤔', 61 + tada: '🎉', 62 + eyes: '👀', 63 + }; 64 + 65 + const renderMessageContent = (content, users, channels, onUserClick) => { 66 + if (!content) return null; 67 + 68 + const parts = content.split(/(<@!?[A-Za-z0-9]+>|<#[A-Za-z0-9]+>|:[a-z0-9_+-]+:)/gi); 69 + 70 + return parts.map((part, index) => { 71 + if (!part) return null; 72 + 73 + const userMention = part.match(/^<@!?([A-Za-z0-9]+)>$/); 74 + if (userMention) { 75 + const userId = userMention[1]; 76 + const user = users[userId]; 77 + return ( 78 + <button 79 + className="mx-0.5 inline rounded bg-[#5865f2]/25 px-1 text-[#bdc3ff] hover:bg-[#5865f2]/35" 80 + key={`${part}-${index}`} 81 + onClick={() => onUserClick(user || { _id: userId, username: 'Unknown user' }, userId)} 82 + type="button" 83 + > 84 + @{user?.username || 'unknown'} 85 + </button> 86 + ); 87 + } 88 + 89 + const channelMention = part.match(/^<#([A-Za-z0-9]+)>$/); 90 + if (channelMention) { 91 + const channelId = channelMention[1]; 92 + return ( 93 + <span className="mx-0.5 inline rounded bg-[#3f4249] px-1 text-gray-100" key={`${part}-${index}`}> 94 + #{channels[channelId]?.name || 'unknown-channel'} 95 + </span> 96 + ); 97 + } 98 + 99 + const emoji = part.match(/^:([a-z0-9_+-]+):$/i); 100 + if (emoji) { 101 + return EMOJI_SHORTCODES[emoji[1].toLowerCase()] || part; 102 + } 103 + 104 + return <React.Fragment key={`${part}-${index}`}>{part}</React.Fragment>; 105 + }); 106 + }; 107 + 49 108 const Avatar = ({ user, cdnUrl, size = 'md' }) => { 50 109 const sizeMap = { 51 110 sm: 'h-8 w-8 text-xs', ··· 87 146 </div> 88 147 ); 89 148 90 - const Message = ({ message, users, me, onUserClick, cdnUrl }) => { 149 + const Message = ({ message, users, channels, me, onUserClick, cdnUrl }) => { 91 150 const authorId = typeof message.author === 'string' ? message.author : message.author?._id; 92 151 const author = users[authorId] || (typeof message.author === 'object' ? message.author : null) || { username: 'Unknown user' }; 93 152 const mine = me === authorId; ··· 105 164 {mine && <span className="rounded bg-[#5865f2]/20 px-1.5 py-0.5 text-[10px] font-semibold text-[#bdc3ff]">YOU</span>} 106 165 <time className="text-[11px] text-gray-500">{toTime(message.createdAt)}</time> 107 166 </div> 108 - {message.content ? <p className="whitespace-pre-wrap break-words text-sm text-gray-200">{message.content}</p> : null} 167 + {message.content ? ( 168 + <p className="whitespace-pre-wrap break-words text-sm text-gray-200">{renderMessageContent(message.content, users, channels, onUserClick)}</p> 169 + ) : null} 109 170 </div> 110 171 </article> 111 172 ); ··· 143 204 const wsRef = useRef(null); 144 205 const messagesBottomRef = useRef(null); 145 206 const subscriptionRef = useRef({}); 207 + const preloadedChannelRef = useRef({}); 208 + const preloadedMembersRef = useRef({}); 146 209 147 210 const serverList = useMemo(() => Object.values(servers), [servers]); 148 211 ··· 267 330 setMessages((prev) => { 268 331 const list = prev[packet.channel] || []; 269 332 if (list.find((m) => m._id === packet._id)) return prev; 270 - return { ...prev, [packet.channel]: [...list, packet] }; 333 + return { ...prev, [packet.channel]: [...list, packet].slice(-200) }; 271 334 }); 272 335 break; 273 336 case 'MessageUpdate': ··· 461 524 }); 462 525 } 463 526 464 - setMessages((prev) => ({ ...prev, [channelId]: payloadMessages.reverse() })); 527 + setMessages((prev) => ({ ...prev, [channelId]: payloadMessages.reverse().slice(-200) })); 528 + preloadedChannelRef.current[channelId] = true; 465 529 } catch { 466 530 // no-op 467 531 } 468 532 }; 469 533 534 + useEffect(() => { 535 + if (status !== 'ready' || !auth.token) return; 536 + 537 + const channelsToWarm = Object.values(channels) 538 + .filter((channel) => channel?.channel_type === 'TextChannel' || !channel?.channel_type) 539 + .slice(0, 25); 540 + 541 + channelsToWarm.forEach((channel, index) => { 542 + if (!channel?._id || preloadedChannelRef.current[channel._id]) return; 543 + setTimeout(() => { 544 + fetchMessages(channel._id); 545 + }, index * 120); 546 + }); 547 + 548 + Object.values(servers).forEach((server) => { 549 + if (!server?._id || preloadedMembersRef.current[server._id]) return; 550 + preloadedMembersRef.current[server._id] = true; 551 + fetchMembers(server._id); 552 + }); 553 + }, [status, auth.token, channels, servers]); 554 + 470 555 const handleServerSelect = (serverId) => { 471 556 setSelectedServerId(serverId); 472 557 ··· 545 630 setChannels({}); 546 631 setUsers({}); 547 632 setMembers({}); 633 + preloadedChannelRef.current = {}; 634 + preloadedMembersRef.current = {}; 548 635 setView('login'); 549 636 setStatus('disconnected'); 550 637 }; ··· 784 871 </div> 785 872 ) : ( 786 873 currentMessages.map((message) => ( 787 - <Message cdnUrl={config.cdnUrl} key={message._id} me={auth.userId} message={message} onUserClick={openUserProfile} users={users} /> 874 + <Message cdnUrl={config.cdnUrl} channels={channels} key={message._id} me={auth.userId} message={message} onUserClick={openUserProfile} users={users} /> 788 875 )) 789 876 )} 790 877 <div ref={messagesBottomRef} />