@@ -40,6 +40,7 @@ import remarkCustomEmoji, {
4040import remarkMentions from "@/shared/lib/remarkMentions" ;
4141import remarkMessageLinks from "@/features/messages/lib/remarkMessageLinks" ;
4242import { Button } from "@/shared/ui/button" ;
43+ import { Popover , PopoverContent , PopoverTrigger } from "@/shared/ui/popover" ;
4344import { Tooltip , TooltipContent , TooltipTrigger } from "@/shared/ui/tooltip" ;
4445
4546import {
@@ -306,6 +307,97 @@ function getCodeBlockText(children: React.ReactNode) {
306307 return getReactNodeText ( children ) . replace ( / \n $ / , "" ) ;
307308}
308309
310+ function InlineEmojiPopover ( {
311+ alt,
312+ resolvedSrc,
313+ } : {
314+ alt : string | undefined ;
315+ resolvedSrc : string ;
316+ } ) {
317+ const [ open , setOpen ] = React . useState ( false ) ;
318+ const openTimeout = React . useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
319+ const closeTimeout = React . useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
320+ const label = alt ?. trim ( ) || "Custom emoji" ;
321+
322+ const clearTimers = React . useCallback ( ( ) => {
323+ if ( openTimeout . current ) {
324+ clearTimeout ( openTimeout . current ) ;
325+ openTimeout . current = null ;
326+ }
327+ if ( closeTimeout . current ) {
328+ clearTimeout ( closeTimeout . current ) ;
329+ closeTimeout . current = null ;
330+ }
331+ } , [ ] ) ;
332+
333+ const handleMouseEnter = React . useCallback ( ( ) => {
334+ clearTimers ( ) ;
335+ openTimeout . current = setTimeout ( ( ) => setOpen ( true ) , 200 ) ;
336+ } , [ clearTimers ] ) ;
337+
338+ const scheduleClose = React . useCallback ( ( ) => {
339+ clearTimers ( ) ;
340+ closeTimeout . current = setTimeout ( ( ) => setOpen ( false ) , 150 ) ;
341+ } , [ clearTimers ] ) ;
342+
343+ const handleFocus = React . useCallback ( ( ) => {
344+ clearTimers ( ) ;
345+ setOpen ( true ) ;
346+ } , [ clearTimers ] ) ;
347+
348+ React . useEffect ( ( ) => clearTimers , [ clearTimers ] ) ;
349+
350+ return (
351+ < Popover open = { open } onOpenChange = { setOpen } >
352+ < PopoverTrigger asChild >
353+ < button
354+ type = "button"
355+ className = "inline-flex border-0 bg-transparent p-0 align-baseline text-inherit"
356+ aria-label = { label }
357+ onMouseEnter = { handleMouseEnter }
358+ onMouseLeave = { scheduleClose }
359+ onFocus = { handleFocus }
360+ onBlur = { scheduleClose }
361+ >
362+ < img
363+ alt = { alt }
364+ title = { label }
365+ src = { resolvedSrc }
366+ data-custom-emoji = ""
367+ className = "mx-px inline-block h-[1.25em] w-auto max-w-none align-text-bottom"
368+ draggable = { false }
369+ onContextMenu = { ( e ) => e . preventDefault ( ) }
370+ />
371+ </ button >
372+ </ PopoverTrigger >
373+ < PopoverContent
374+ align = "center"
375+ side = "top"
376+ sideOffset = { 6 }
377+ className = "w-auto min-w-32 max-w-56 rounded-xl p-3"
378+ onMouseEnter = { handleMouseEnter }
379+ onMouseLeave = { scheduleClose }
380+ onOpenAutoFocus = { ( e ) => e . preventDefault ( ) }
381+ onCloseAutoFocus = { ( e ) => e . preventDefault ( ) }
382+ >
383+ < div className = "flex flex-col items-center text-center" >
384+ < div className = "mb-2 flex h-14 w-14 items-center justify-center" >
385+ < img
386+ alt = { alt }
387+ src = { resolvedSrc }
388+ className = "inline-block h-12 w-12 object-contain"
389+ draggable = { false }
390+ />
391+ </ div >
392+ < div className = "max-w-[12rem] text-balance text-sm font-semibold leading-snug text-popover-foreground" >
393+ { label }
394+ </ div >
395+ </ div >
396+ </ PopoverContent >
397+ </ Popover >
398+ ) ;
399+ }
400+
309401function MarkdownCodeBlock ( {
310402 children,
311403 language,
@@ -839,18 +931,10 @@ function createMarkdownComponents(
839931 if ( ! resolvedSrc ) {
840932 return < span > { alt } </ span > ;
841933 }
842- // Inline custom emoji: sized to the line, baseline-aligned with text.
843- return (
844- < img
845- alt = { alt }
846- title = { alt }
847- src = { resolvedSrc }
848- data-custom-emoji = ""
849- className = "mx-px inline-block h-[1.25em] w-auto max-w-none align-text-bottom"
850- draggable = { false }
851- onContextMenu = { ( e ) => e . preventDefault ( ) }
852- />
853- ) ;
934+ if ( ! interactive ) {
935+ return < span > { alt } </ span > ;
936+ }
937+ return < InlineEmojiPopover alt = { alt } resolvedSrc = { resolvedSrc } /> ;
854938 } ,
855939 "channel-link" : ( { children } : { children ?: React . ReactNode } ) => {
856940 const text = String ( children ?? "" ) ;
0 commit comments