[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 #16 from FascinatingPistachio/codex/develop-complete-branding-system-for-ermine

Codex-generated pull request

authored by

Fascinating Pistachio and committed by
GitHub
(Feb 18, 2026, 4:22 PM UTC) 229188c7 5db86dff

+22 -13
+22 -13
src/App.jsx
··· 380 380 if (userMention) { 381 381 const userId = userMention[1]; 382 382 const user = users[userId]; 383 - return <button className="mx-0.5 inline rounded bg-[#5865f2]/25 px-1 text-[#bdc3ff] hover:bg-[#5865f2]/35" key={`${part}-${index}`} onClick={() => onUserClick(user || { _id: userId, username: 'Unknown user' }, userId)}>@{user?.username || 'unknown'}</button>; 383 + return <button className="mx-0.5 inline rounded bg-[#5865f2]/25 px-1 text-[#bdc3ff] hover:bg-[#5865f2]/35" key={`${part}-${index}`} onClick={() => onUserClick(user || { _id: userId, username: 'Loading user…', _loading: true }, userId)}>@{user?.username || 'loading-user'}</button>; 384 384 } 385 385 const channelMention = part.match(/^<#([A-Za-z0-9]+)>$/); 386 386 if (channelMention) return <span className="mx-0.5 inline rounded bg-[#3f4249] px-1 text-gray-100" key={`${part}-${index}`}>#{channels[channelMention[1]]?.name || 'unknown-channel'}</span>; ··· 472 472 onEditMessage, onDeleteMessage, replyMessageMap 473 473 }) => { 474 474 const authorId = typeof message.author === 'string' ? message.author : message.author?._id; 475 - const author = users[authorId] || (typeof message.author === 'object' ? message.author : null) || { username: 'Unknown user' }; 475 + const author = users[authorId] || (typeof message.author === 'object' ? message.author : null) || { _id: authorId, username: 'Loading user…', _loading: true }; 476 476 const mine = me === authorId; 477 477 const messageReactions = toReactionEntries(message.reactions); 478 478 ··· 556 556 type="button" 557 557 > 558 558 <Reply size={12} /> 559 - <span className="truncate">Replying to {replyUser?.username || 'Unknown user'}: {fullReplyMessage?.content || message.replyMessage?.content || 'Attachment / embed'}</span> 559 + <span className="truncate">Replying to {replyUser?.username || 'Loading user…'}: {fullReplyMessage?.content || message.replyMessage?.content || 'Attachment / embed'}</span> 560 560 </button> 561 561 ) : null} 562 562 ··· 683 683 // Define openUserProfile callback first since it is used in other helpers 684 684 const openUserProfile = useCallback((user, fallbackId = null) => { 685 685 const stableId = user?._id || fallbackId || 'unknown-user'; 686 - setPeekUser({ ...user, _id: stableId, username: user?.username || 'Unknown user', discriminator: user?.discriminator || '0000' }); 686 + setPeekUser({ ...user, _id: stableId, username: user?.username || 'Loading user…', discriminator: user?.discriminator || '0000' }); 687 687 }, []); 688 688 689 689 const [servers, setServers] = useState({}); ··· 766 766 const renderMemberItem = useCallback((item) => { 767 767 if (item.type === 'header') return <div className="pt-4 pb-1 px-4 text-xs font-bold uppercase tracking-wide text-gray-400 flex items-center gap-1">{item.name} — {item.count}</div>; 768 768 const member = item.data; 769 + const memberUser = member.user || { _id: member?._id?.user, username: 'Loading user…', _loading: true }; 770 + const isUserLoading = !member.user; 769 771 return ( 770 - <button className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left hover:bg-[#35373c]" onClick={() => openUserProfile(member.user, member._id.user)}> 771 - <Avatar animateOnHover cdnUrl={config.cdnUrl} size="sm" user={member.user} /> 772 - <div className="min-w-0"> 773 - <div className="truncate text-sm font-medium" style={{ color: member.color || '#f2f3f5' }}>{member.nickname || member.user.username}</div> 774 - <div className="flex items-center gap-1 text-[11px] text-gray-500"><User size={11} /> {member.user.status?.presence || 'Offline'}</div> 772 + <button className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left hover:bg-[#35373c]" onClick={() => openUserProfile(memberUser, member._id.user)}> 773 + <Avatar animateOnHover cdnUrl={config.cdnUrl} size="sm" user={memberUser} /> 774 + <div className="min-w-0 flex-1"> 775 + <div className="truncate text-sm font-medium" style={{ color: member.color || '#f2f3f5' }}>{member.nickname || memberUser.username}</div> 776 + {isUserLoading ? ( 777 + <div className="mt-1 h-1.5 w-24 overflow-hidden rounded-full bg-[#242A35]"><div className="h-full w-1/2 animate-pulse rounded-full bg-[#8AB4F8]" /></div> 778 + ) : ( 779 + <div className="flex items-center gap-1 text-[11px] text-gray-500"><User size={11} /> {memberUser.status?.presence || 'Offline'}</div> 780 + )} 775 781 </div> 776 782 </button> 777 783 ); ··· 1256 1262 <div className="flex items-center gap-3 p-3"> 1257 1263 <Avatar alwaysAnimate cdnUrl={config.cdnUrl} size="lg" user={peekUser} /> 1258 1264 <div> 1259 - <div className="text-base font-semibold text-white">{peekUser?.username || 'Unknown user'}</div> 1265 + <div className="text-base font-semibold text-white">{peekUser?.username || 'Loading user…'}</div> 1260 1266 <div className="text-xs text-gray-400">#{peekUser?.discriminator || '0000'}</div> 1261 1267 </div> 1262 1268 </div> ··· 1327 1333 {directMessageChannels.map((channel) => { 1328 1334 const recipientId = (channel.recipients || []).find((id) => id !== auth.userId); 1329 1335 const recipient = users[recipientId]; 1330 - const label = recipient?.username || channel.name || 'Direct Message'; 1336 + const label = recipient?.username || channel.name || 'Loading direct…'; 1331 1337 return ( 1332 1338 <button className={`mb-0.5 flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm ${selectedChannelId === channel._id ? 'bg-[#404249] text-white' : 'text-gray-400 hover:bg-[#35373c] hover:text-white'}`} key={channel._id} onClick={() => handleChannelSelect(channel._id)}> 1333 1339 <MessageSquare size={16} /> ··· 1444 1450 {activeReply ? ( 1445 1451 <div className="mb-2 flex items-center justify-between gap-2 rounded-md bg-[#2b2d31] px-3 py-2 text-xs text-gray-300"> 1446 1452 <span className="truncate"> 1447 - Replying to {users[typeof activeReply.author === 'string' ? activeReply.author : activeReply.author?._id]?.username || 'Unknown user'}: {activeReply.content || 'Attachment / embed'} 1453 + Replying to {users[typeof activeReply.author === 'string' ? activeReply.author : activeReply.author?._id]?.username || 'Loading user…'}: {activeReply.content || 'Attachment / embed'} 1448 1454 </span> 1449 1455 <button className="rounded p-1 text-gray-400 hover:bg-[#3a3d42] hover:text-white" onClick={() => setReplyingTo(null)} type="button"> 1450 1456 <X size={13} /> ··· 1514 1520 </main> 1515 1521 1516 1522 <aside className="hidden w-60 flex-col border-l border-[#202225] bg-[#2b2d31] lg:flex"> 1517 - <div className="border-b border-[#202225] px-4 py-3 text-xs font-bold uppercase tracking-wide text-gray-400">Members — {allCurrentMembers.length}{isMembersLoading ? ' (loading...)' : ''}</div> 1523 + <div className="border-b border-[#202225] px-4 py-3 text-xs font-bold uppercase tracking-wide text-gray-400"> 1524 + <div>Members — {allCurrentMembers.length}</div> 1525 + {isMembersLoading ? <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-[#242A35]"><div className="h-full w-1/3 animate-pulse rounded-full bg-[#8AB4F8]" /></div> : null} 1526 + </div> 1518 1527 <div className="min-h-0 flex-1 relative"> 1519 1528 <VirtualList items={memberListItems} itemHeight={40} className="h-full w-full" renderItem={renderMemberItem} /> 1520 1529 </div>