1+ import * as React from 'react' ;
2+ import { useRoomContext } from '../../context' ;
3+ import { setupDataMessageHandler } from '@livekit/components-core' ;
4+ import { DataTopic } from '@livekit/components-core' ;
5+ import { mergeProps } from '../../utils' ;
6+ import { useEmojiReactionContext } from '../../context/EmojiReactionContext' ;
7+
8+ const EMOJI_OPTIONS = [ '👍' , '👎' , '❤️' , '😂' , '😮' , '😢' , '👏' , '🎉' ] ;
9+
10+ export interface EmojiReactionButtonProps extends React . HTMLAttributes < HTMLButtonElement > {
11+ showIcon ?: boolean ;
12+ showText ?: boolean ;
13+ }
14+
15+ export function EmojiReactionButton ( {
16+ showIcon = true ,
17+ showText = false ,
18+ ...props
19+ } : EmojiReactionButtonProps ) {
20+ const room = useRoomContext ( ) ;
21+ const { addReaction } = useEmojiReactionContext ( ) ;
22+ const [ isOpen , setIsOpen ] = React . useState ( false ) ;
23+ const buttonRef = React . useRef < HTMLButtonElement > ( null ) ;
24+ const popupRef = React . useRef < HTMLDivElement > ( null ) ;
25+
26+ const { send, isSendingObservable } = React . useMemo (
27+ ( ) => setupDataMessageHandler ( room , DataTopic . REACTIONS ) ,
28+ [ room ] ,
29+ ) ;
30+
31+ const [ isSending , setIsSending ] = React . useState ( false ) ;
32+
33+ React . useEffect ( ( ) => {
34+ const subscription = isSendingObservable . subscribe ( setIsSending ) ;
35+ return ( ) => subscription . unsubscribe ( ) ;
36+ } , [ isSendingObservable ] ) ;
37+
38+ React . useEffect ( ( ) => {
39+ const handleClickOutside = ( event : MouseEvent ) => {
40+ if (
41+ buttonRef . current &&
42+ popupRef . current &&
43+ ! buttonRef . current . contains ( event . target as Node ) &&
44+ ! popupRef . current . contains ( event . target as Node )
45+ ) {
46+ setIsOpen ( false ) ;
47+ }
48+ } ;
49+
50+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
51+ return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
52+ } , [ ] ) ;
53+
54+ const handleEmojiClick = React . useCallback (
55+ async ( emoji : string ) => {
56+ if ( isSending ) return ;
57+
58+ try {
59+ const payload = new TextEncoder ( ) . encode ( JSON . stringify ( { emoji } ) ) ;
60+ await send ( payload ) ;
61+
62+ // Add local reaction immediately
63+ addReaction ( {
64+ emoji,
65+ from : room . localParticipant ,
66+ } ) ;
67+
68+ // Don't close the popup - let user send multiple reactions
69+ } catch ( error ) {
70+ console . error ( 'Failed to send emoji reaction:' , error ) ;
71+ }
72+ } ,
73+ [ send , isSending , room . localParticipant , addReaction ] ,
74+ ) ;
75+
76+ const htmlProps = mergeProps (
77+ {
78+ className : 'lk-button lk-emoji-reaction-button' ,
79+ onClick : ( ) => setIsOpen ( ! isOpen ) ,
80+ disabled : isSending ,
81+ } ,
82+ props ,
83+ ) ;
84+
85+ return (
86+ < div className = "lk-button-group" >
87+ < button ref = { buttonRef } { ...htmlProps } >
88+ { showIcon && < span > 😊</ span > }
89+ { showText && 'Reactions' }
90+ </ button >
91+ { isOpen && (
92+ < div ref = { popupRef } className = "lk-emoji-popup" >
93+ { EMOJI_OPTIONS . map ( ( emoji ) => (
94+ < button
95+ key = { emoji }
96+ className = "lk-emoji-option"
97+ onClick = { ( ) => handleEmojiClick ( emoji ) }
98+ disabled = { isSending }
99+ >
100+ { emoji }
101+ </ button >
102+ ) ) }
103+ </ div >
104+ ) }
105+ </ div >
106+ ) ;
107+ }
0 commit comments