[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.

Reduce member panel load to prevent server-open freezes

Fascinating Pistachio (Feb 14, 2026, 2:57 PM UTC) 4e2b0c7e fa3b3e81

+52 -26
+52 -26
src/App.jsx
··· 65 65 }; 66 66 67 67 const QUICK_EMOJIS = ['๐Ÿ‘', 'โค๏ธ', '๐Ÿ˜‚', '๐Ÿ”ฅ', '๐Ÿ˜ญ', '๐ŸŽ‰', '๐Ÿ˜ฎ', '๐Ÿ‘€', '๐Ÿค”']; 68 + const MEMBER_RENDER_LIMIT = 250; 69 + const MEMBER_HYDRATE_CHUNK = 400; 68 70 69 71 const toReactionEntries = (reactions) => { 70 72 if (!reactions || typeof reactions !== 'object') return []; ··· 276 278 const [activeModal, setActiveModal] = useState(null); 277 279 const [createServerName, setCreateServerName] = useState(''); 278 280 const [peekUser, setPeekUser] = useState(null); 281 + const [isMembersLoading, setIsMembersLoading] = useState(false); 279 282 const [replyingTo, setReplyingTo] = useState(null); 280 283 const [showEmojiPicker, setShowEmojiPicker] = useState(false); 281 284 ··· 293 296 const preloadedChannelRef = useRef({}); 294 297 const preloadedMembersRef = useRef({}); 295 298 const pendingUserFetchRef = useRef(new Set()); 299 + const membersLoadIdRef = useRef(0); 296 300 297 301 const serverList = useMemo(() => Object.values(servers), [servers]); 298 302 ··· 304 308 const currentMessages = useMemo(() => messages[selectedChannelId] || [], [messages, selectedChannelId]); 305 309 const currentMessageMap = useMemo(() => Object.fromEntries(currentMessages.map((message) => [message._id, message])), [currentMessages]); 306 310 307 - const currentMembers = useMemo(() => { 311 + const allCurrentMembers = useMemo(() => { 308 312 if (selectedServerId === '@me') return []; 309 313 return Object.values(members) 310 - .filter((m) => m._id.server === selectedServerId) 311 - .map((m) => ({ ...m, user: users[m._id.user] })) 312 - .filter((m) => Boolean(m.user)) 313 - .sort((a, b) => a.user.username.localeCompare(b.user.username)); 314 + .filter((member) => member._id.server === selectedServerId) 315 + .map((member) => ({ ...member, user: users[member._id.user] })) 316 + .filter((member) => Boolean(member.user)); 314 317 }, [members, selectedServerId, users]); 318 + 319 + const currentMembers = useMemo( 320 + () => allCurrentMembers.slice(0, MEMBER_RENDER_LIMIT).sort((a, b) => (a.nickname || a.user.username).localeCompare(b.nickname || b.user.username)), 321 + [allCurrentMembers], 322 + ); 323 + 324 + const hiddenMembersCount = Math.max(0, allCurrentMembers.length - currentMembers.length); 315 325 316 326 const openUserProfile = (user, fallbackId = null) => { 317 327 const stableId = user?._id || fallbackId || 'unknown-user'; ··· 614 624 const fetchMembers = async (serverId) => { 615 625 if (serverId === '@me') return; 616 626 627 + const loadId = Date.now(); 628 + membersLoadIdRef.current = loadId; 629 + setIsMembersLoading(true); 630 + 617 631 try { 618 632 const res = await fetch(`${config.apiUrl}/servers/${serverId}/members`, { 619 633 headers: { 'x-session-token': auth.token }, ··· 621 635 if (!res.ok) return; 622 636 const data = await res.json(); 623 637 624 - setUsers((prev) => { 625 - const next = { ...prev }; 626 - data.users?.forEach((u) => { 627 - next[u._id] = u; 628 - }); 629 - return next; 630 - }); 631 - setMembers((prev) => { 632 - const next = { ...prev }; 633 - data.members?.forEach((m) => { 634 - next[`${m._id.server}:${m._id.user}`] = m; 638 + upsertUsers(data.users || []); 639 + const payloadMembers = data.members || []; 640 + 641 + for (let index = 0; index < payloadMembers.length; index += MEMBER_HYDRATE_CHUNK) { 642 + if (membersLoadIdRef.current !== loadId) break; 643 + const chunk = payloadMembers.slice(index, index + MEMBER_HYDRATE_CHUNK); 644 + 645 + setMembers((prev) => { 646 + const next = { ...prev }; 647 + chunk.forEach((member) => { 648 + next[`${member._id.server}:${member._id.user}`] = member; 649 + }); 650 + return next; 635 651 }); 636 - return next; 637 - }); 652 + 653 + if (index + MEMBER_HYDRATE_CHUNK < payloadMembers.length) { 654 + await new Promise((resolve) => setTimeout(resolve, 0)); 655 + } 656 + } 638 657 } catch { 639 658 // no-op 659 + } finally { 660 + if (membersLoadIdRef.current === loadId) { 661 + setIsMembersLoading(false); 662 + } 640 663 } 641 664 }; 642 665 ··· 705 728 }, index * 350); 706 729 }); 707 730 708 - Object.values(servers).forEach((server) => { 709 - if (!server?._id || preloadedMembersRef.current[server._id]) return; 710 - preloadedMembersRef.current[server._id] = true; 711 - fetchMembers(server._id); 712 - }); 713 - }, [status, auth.token, channels, servers]); 731 + }, [status, auth.token, channels]); 714 732 715 733 const handleServerSelect = (serverId) => { 716 734 setSelectedServerId(serverId); ··· 725 743 const firstChannel = Object.values(channels).find((ch) => ch.server === serverId); 726 744 setSelectedChannelId(firstChannel?._id || null); 727 745 if (firstChannel?._id) fetchMessages(firstChannel._id); 728 - fetchMembers(serverId); 746 + if (!preloadedMembersRef.current[serverId]) { 747 + preloadedMembersRef.current[serverId] = true; 748 + void fetchMembers(serverId); 749 + } 729 750 }; 730 751 731 752 const handleChannelSelect = (channelId) => { ··· 797 818 preloadedChannelRef.current = {}; 798 819 preloadedMembersRef.current = {}; 799 820 pendingUserFetchRef.current = new Set(); 821 + membersLoadIdRef.current = 0; 822 + setIsMembersLoading(false); 800 823 setView('login'); 801 824 setStatus('disconnected'); 802 825 }; ··· 1146 1169 </main> 1147 1170 1148 1171 <aside className="hidden w-60 flex-col border-l border-[#202225] bg-[#2b2d31] lg:flex"> 1149 - <div className="border-b border-[#202225] px-4 py-3 text-xs font-bold uppercase tracking-wide text-gray-400">Members โ€” {currentMembers.length}</div> 1172 + <div className="border-b border-[#202225] px-4 py-3 text-xs font-bold uppercase tracking-wide text-gray-400"> 1173 + Members โ€” {allCurrentMembers.length}{isMembersLoading ? ' (loading...)' : ''} 1174 + </div> 1150 1175 <div className="min-h-0 flex-1 space-y-1 overflow-y-auto p-2"> 1176 + {hiddenMembersCount > 0 ? <p className="px-2 py-1 text-[11px] text-gray-500">Showing first {currentMembers.length} members for responsiveness.</p> : null} 1151 1177 {currentMembers.map((member) => ( 1152 1178 <button className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left hover:bg-[#35373c]" key={member._id.user} onClick={() => openUserProfile(member.user, member._id.user)}> 1153 1179 <Avatar cdnUrl={config.cdnUrl} size="sm" user={member.user} />