import { useCallback, useEffect, useRef, useState } from 'react'; import { MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk'; import { BurstPoint, resolveBurstOrigin } from './burstOrigin'; import { EMOJI_CONFETTI_EVENT_TYPE, EmojiConfettiBurst, EmojiConfettiContent, } from './types'; function scheduleBurstFromValues( burstId: string, emojis: string[], targetEventId: string | undefined, onBurst: (burst: EmojiConfettiBurst) => void, attempt = 0, explicitOrigin?: BurstPoint ) { const targetElement = targetEventId && document.querySelector(`[data-message-id="${CSS.escape(targetEventId)}"]`); if (targetEventId && !targetElement && attempt < 8) { window.setTimeout( () => scheduleBurstFromValues( burstId, emojis, targetEventId, onBurst, attempt + 1, explicitOrigin ), 50 ); return; } onBurst({ id: burstId, targetEventId: targetEventId ?? '', emojis: emojis.length > 0 ? emojis : ['🎉'], origin: resolveBurstOrigin(targetEventId, emojis, explicitOrigin), }); } function scheduleBurst( event: MatrixEvent, onBurst: (burst: EmojiConfettiBurst) => void, attempt = 0 ) { const eventId = event.getId(); if (!eventId) return; const { emojis, targetEventId } = parseEmojiConfettiContent(event.getContent()); scheduleBurstFromValues(eventId, emojis, targetEventId, onBurst, attempt); } function parseEmojiConfettiContent(content: unknown): { emojis: string[]; targetEventId?: string } { const confettiContent = content as EmojiConfettiContent; const emojis = Array.isArray(confettiContent.emojis) ? confettiContent.emojis.filter((emoji): emoji is string => typeof emoji === 'string' && emoji.length > 0) : []; return { emojis: emojis.length > 0 ? emojis : ['🎉'], targetEventId: typeof confettiContent.target_event_id === 'string' ? confettiContent.target_event_id : undefined, }; } export function useEmojiConfetti(room: Room) { const [bursts, setBursts] = useState([]); const seenEventIdsRef = useRef(new Set()); const ownEventIdsRef = useRef(new Set()); const addBurst = useCallback((burst: EmojiConfettiBurst) => { setBursts((current) => [...current, burst]); }, []); const registerOwnEventId = useCallback((eventId: string) => { ownEventIdsRef.current.add(eventId); }, []); const queueBurst = useCallback((event: MatrixEvent) => { const eventId = event.getId(); if (!eventId || seenEventIdsRef.current.has(eventId)) return; seenEventIdsRef.current.add(eventId); if (ownEventIdsRef.current.has(eventId)) { ownEventIdsRef.current.delete(eventId); return; } scheduleBurst(event, addBurst); }, [addBurst]); const triggerLocalBurst = useCallback( (targetEventId: string, emojis: string[], origin?: BurstPoint) => { const burstId = `local-${targetEventId}-${Date.now()}`; scheduleBurstFromValues(burstId, emojis, targetEventId, addBurst, 0, origin); }, [addBurst] ); useEffect(() => { const handleTimeline: ( event: MatrixEvent, eventRoom: Room | undefined, toStartOfTimeline?: boolean, removed?: boolean ) => void = (event, eventRoom, toStartOfTimeline, removed) => { if (removed || toStartOfTimeline || eventRoom?.roomId !== room.roomId) return; if (event.getType() !== EMOJI_CONFETTI_EVENT_TYPE) return; queueBurst(event); }; room.on(RoomEvent.Timeline, handleTimeline); return () => { room.off(RoomEvent.Timeline, handleTimeline); }; }, [queueBurst, room]); const removeBurst = useCallback((burstId: string) => { setBursts((current) => current.filter((burst) => burst.id !== burstId)); }, []); return { bursts, removeBurst, triggerLocalBurst, registerOwnEventId, }; }