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

Improve message-hover avatar animation and custom emoji fallback

Fascinating Pistachio (Feb 14, 2026, 3:55 PM UTC) b8fab98e e8039115

+22 -12
+22 -12
src/App.jsx
··· 144 144 145 145 const getCustomEmojiId = (token) => token?.slice(1, -1); 146 146 147 + const resolveCustomEmojiMeta = (token, customEmojiById) => { 148 + if (!isCustomEmojiToken(token)) return null; 149 + const emojiId = getCustomEmojiId(token); 150 + const mapped = customEmojiById?.[emojiId]; 151 + return mapped || { id: emojiId, name: emojiId, serverName: null, isPrivate: false, unresolved: true }; 152 + }; 153 + 147 154 const renderEmojiVisual = (token, customEmoji, cdnUrl) => { 148 - if (customEmoji?.id) { 149 - return <img alt={customEmoji.name || token} className="inline h-5 w-5 align-text-bottom" src={`${cdnUrl}/emojis/${customEmoji.id}`} />; 155 + const emojiId = customEmoji?.id || (isCustomEmojiToken(token) ? getCustomEmojiId(token) : null); 156 + if (emojiId) { 157 + return <img alt={customEmoji?.name || token} className="inline h-5 w-5 align-text-bottom" src={`${cdnUrl}/emojis/${emojiId}`} />; 150 158 } 151 159 return token; 152 160 }; ··· 204 212 const emoji = part.match(/^:([a-z0-9_+-]+):$/i); 205 213 if (emoji) { 206 214 if (isCustomEmojiToken(part)) { 207 - const customEmoji = customEmojiById[getCustomEmojiId(part)]; 208 - const source = customEmoji?.isPrivate ? 'Private server emoji' : customEmoji?.serverName ? `Server emoji from ${customEmoji.serverName}` : 'Custom emoji'; 215 + const customEmoji = resolveCustomEmojiMeta(part, customEmojiById); 216 + const source = customEmoji?.unresolved ? 'Custom emoji (external/unavailable metadata)' : customEmoji?.isPrivate ? 'Private server emoji' : customEmoji?.serverName ? `Server emoji from ${customEmoji.serverName}` : 'Custom emoji'; 209 217 return ( 210 218 <span className="mx-0.5 inline-flex items-center" key={`${part}-${index}`} title={`${customEmoji?.name || part} · ${source}`}> 211 219 {renderEmojiVisual(part, customEmoji, cdnUrl)} ··· 315 323 const messageReactions = toReactionEntries(message.reactions); 316 324 const replyPreview = message.replyMessage; 317 325 const [showReactionPicker, setShowReactionPicker] = useState(false); 326 + const [isMessageHovered, setIsMessageHovered] = useState(false); 318 327 319 328 return ( 320 - <article className="group flex gap-3 rounded px-4 py-2 hover:bg-[#2e3035]" ref={(node) => registerMessageRef(message._id, node)}> 329 + <article className="group flex gap-3 rounded px-4 py-2 hover:bg-[#2e3035]" onMouseEnter={() => setIsMessageHovered(true)} onMouseLeave={() => setIsMessageHovered(false)} ref={(node) => registerMessageRef(message._id, node)}> 321 330 <button className="mt-0.5" onClick={() => onUserClick(author, authorId)} type="button"> 322 - <Avatar animateOnHover cdnUrl={cdnUrl} user={author} /> 331 + <Avatar alwaysAnimate={isMessageHovered} cdnUrl={cdnUrl} user={author} /> 323 332 </button> 324 333 <div className="min-w-0 flex-1"> 325 334 {replyPreview ? ( ··· 373 382 <div className="mt-1 flex flex-wrap items-center gap-1.5"> 374 383 {messageReactions.map(({ emoji, userIds }) => { 375 384 const reacted = userIds.includes(me); 376 - const customEmoji = isCustomEmojiToken(emoji) ? customEmojiById[getCustomEmojiId(emoji)] : null; 377 - const source = customEmoji?.isPrivate ? 'Private server emoji' : customEmoji?.serverName ? `Server emoji from ${customEmoji.serverName}` : 'Emoji'; 385 + const customEmoji = resolveCustomEmojiMeta(emoji, customEmojiById); 386 + const source = customEmoji?.unresolved ? 'Custom emoji (external/unavailable metadata)' : customEmoji?.isPrivate ? 'Private server emoji' : customEmoji?.serverName ? `Server emoji from ${customEmoji.serverName}` : 'Emoji'; 378 387 return ( 379 388 <button 380 389 className={`rounded-full border px-2 py-0.5 text-xs ${reacted ? 'border-[#5865f2] bg-[#5865f2]/20 text-[#d7ddff]' : 'border-[#4c4f56] bg-[#2b2d31] text-gray-200 hover:bg-[#35373c]'}`} ··· 395 404 + React 396 405 </button> 397 406 {showReactionPicker ? ( 398 - <div className="absolute bottom-7 left-0 z-30 w-56 rounded-lg border border-[#4c4f56] bg-[#232428] p-2 shadow-2xl"> 399 - <div className="grid grid-cols-6 gap-1"> 407 + <div className="absolute bottom-7 left-0 z-30 w-72 rounded-lg border border-[#4c4f56] bg-[#232428] p-2 shadow-2xl"> 408 + <p className="mb-1 px-1 text-[10px] uppercase tracking-wide text-gray-500">Add reaction</p> 409 + <div className="grid max-h-44 grid-cols-8 gap-1 overflow-y-auto"> 400 410 {reactionOptions.map((option) => ( 401 411 <button 402 412 className="grid h-8 place-items-center rounded hover:bg-[#3a3d42]" ··· 1790 1800 <Smile size={16} /> 1791 1801 </button> 1792 1802 {showEmojiPicker && selectedChannelId !== 'friends' ? ( 1793 - <div className="absolute bottom-12 left-0 z-20 w-60 rounded-lg border border-[#4c4f56] bg-[#232428] p-2 shadow-2xl"> 1803 + <div className="absolute bottom-12 left-0 z-20 w-72 rounded-lg border border-[#4c4f56] bg-[#232428] p-2 shadow-2xl"> 1794 1804 <p className="mb-1 text-[10px] uppercase tracking-wide text-gray-500">Emoji</p> 1795 - <div className="grid max-h-36 grid-cols-6 gap-1 overflow-y-auto"> 1805 + <div className="grid max-h-36 grid-cols-8 gap-1 overflow-y-auto"> 1796 1806 {EXTENDED_EMOJIS.map((emoji) => ( 1797 1807 <button className="rounded p-1 text-lg hover:bg-[#3a3d42]" key={emoji} onClick={() => addEmojiToComposer(emoji)} type="button"> 1798 1808 {emoji}