Add emoji confetti bursts and component playground.
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.
This commit is contained in:
137
src/app/features/room/emoji-confetti/useEmojiConfetti.ts
Normal file
137
src/app/features/room/emoji-confetti/useEmojiConfetti.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user