Support app.relay.emoji_confetti on jumbo emoji clicks with canvas physics, per-emoji particle profiles, and a fixed overlay that does not affect scroll. Also add a Vite playground for live component previews and theme hover preview.
138 lines
3.8 KiB
TypeScript
138 lines
3.8 KiB
TypeScript
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<EmojiConfettiBurst[]>([]);
|
|
const seenEventIdsRef = useRef(new Set<string>());
|
|
const ownEventIdsRef = useRef(new Set<string>());
|
|
|
|
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,
|
|
};
|
|
}
|