Skip to content

Commit

Permalink
add ids to incoming bc messages + id clears
Browse files Browse the repository at this point in the history
  • Loading branch information
codyzu committed Jul 19, 2023
1 parent 4724a66 commit 1111fd6
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 163 deletions.
14 changes: 14 additions & 0 deletions src/components/broadcast/use-broadcast-channel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useEffect, useState} from 'react';
import {nanoid} from 'nanoid';
import {type Handler, type Payload} from './use-channel-handlers';

export default function useBroadcastChannel({
Expand All @@ -18,6 +19,19 @@ export default function useBroadcastChannel({
const channelMessageHandler = (event: MessageEvent) => {
console.log('incoming bc', channelId, event);
if (onIncoming) {
const incomingPayload = event.data as Payload;

if (incomingPayload.id === 'reactions') {
onIncoming({
id: 'reactions',
reactions: incomingPayload.reactions.map(([, reaction]) => [
nanoid(),
reaction,
]),
});
return;
}

onIncoming(event.data as Payload);
}
};
Expand Down
58 changes: 31 additions & 27 deletions src/components/broadcast/use-broadcast-firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'firebase/firestore';
import {firestore as db} from '../../firebase';
import {
type ReactionEntry,
type IncomingReactionData,
type ReactionData,
} from '../reactions/reaction';
Expand Down Expand Up @@ -37,14 +38,17 @@ export default function useBroadcastFirebase({
// TTL 1 hour
const ttl = new Date(Date.now() + 60 * 60 * 1000);

if (payload.id === 'reaction') {
// Ignore the id when posting, it will get generated
const [, reaction] = payload.reaction;
return addDoc(collection(db, 'sessions', sessionId, 'reactions'), {
reaction,
ttl,
created: serverTimestamp(),
} satisfies ReactionData);
if (payload.id === 'reactions') {
return Promise.all(
// Ignore the id when posting, it will get generated
payload.reactions.map(async ([, reaction]) => {
return addDoc(collection(db, 'sessions', sessionId, 'reactions'), {
reaction,
ttl,
created: serverTimestamp(),
} satisfies ReactionData);
}),
);
}

if (payload.id === 'slide index') {
Expand Down Expand Up @@ -82,26 +86,26 @@ export default function useBroadcastFirebase({

const now = Date.now();

for (const change of next.docChanges()) {
if (change.type !== 'added') {
continue;
}

const reactionData = change.doc.data({
serverTimestamps: 'estimate',
}) as IncomingReactionData;

console.log('data', reactionData);
const reactions = next.docs
.map((snapshot) => {
const reactionData = snapshot.data({
serverTimestamps: 'estimate',
}) as IncomingReactionData;
return {
id: snapshot.id,
reaction: reactionData.reaction,
created: reactionData.created.toMillis(),
};
})
// Ignore reactions older than 30 seconds
if (now - reactionData.created.toMillis() > 30_000) {
continue;
}

onIncoming({
id: 'reaction',
reaction: [change.doc.id, reactionData.reaction],
});
}
.filter((reactionDoc) => now - reactionDoc.created < 30_000)
.sort((a, b) => a.created - b.created)
.map(
(reactionDoc) =>
[reactionDoc.id, reactionDoc.reaction] satisfies ReactionEntry,
);

onIncoming({id: 'reactions', reactions});
},
(error) => {
setConnected(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/broadcast/use-channel-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export type Payload =
icon?: never;
}
| {
id: 'reaction';
reaction: ReactionEntry;
id: 'reactions';
reactions: ReactionEntry[];
};
export type Handler = (payload: Payload) => void;

Expand Down
97 changes: 71 additions & 26 deletions src/components/confetti/Confetti.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,81 @@
import {useEffect, useRef, useState} from 'react';
import ReactCanvasConfetti from 'react-canvas-confetti';
import {type ConfettiReactionEntry} from '../reactions/reaction';

function confettiProps(): confetti.Options {
return {
angle: 90,
colors: [
'#26ccff',
'#a25afd',
'#ff5e7e',
'#88ff5a',
'#fcff42',
'#ffa62d',
'#ff36ff',
],
decay: 0.8,
drift: 0,
gravity: 1,
origin: {
x: Math.random(),
y: Math.random(),
},
particleCount: 500,
scalar: 1,
shapes: ['circle', 'square', 'star'],
spread: 360,
startVelocity: 45,
ticks: 600,
};
}

export default function Confetti({
confettiReactions,
clear,
fire,
}: {
confettiReactions: ConfettiReactionEntry[];
clear?: any;
fire?: any;
}) {
const [doneReactions, setDoneReactions] = useState<string[]>([]);
const confettiRef = useRef<confetti.CreateTypes>();
useEffect(() => {
for (const [id, confettiReaction] of confettiReactions) {
if (doneReactions.includes(id)) {
continue;
}

if (confettiReaction === 'confetti') {
void confettiRef.current?.(confettiProps());
}

setDoneReactions((currentDoneReactions) => [...currentDoneReactions, id]);
}
}, [confettiReactions, doneReactions]);

useEffect(() => {
if (clear) {
console.log('clearing');
confettiRef.current?.reset();
}
}, [clear]);

useEffect(() => {
if (fire) {
void confettiRef.current?.(confettiProps());
}
}, [fire]);

export default function Confetti({fire, reset}: {fire: any; reset?: any}) {
return (
<ReactCanvasConfetti
resize
useWorker
fire={fire} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
reset={reset} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
angle={90}
className="position-fixed top-0 left-0 w-screen h-screen pointer-events-none"
colors={[
'#26ccff',
'#a25afd',
'#ff5e7e',
'#88ff5a',
'#fcff42',
'#ffa62d',
'#ff36ff',
]}
decay={0.8}
drift={0}
gravity={1}
origin={{
x: Math.random(),
y: Math.random(),
refConfetti={(confetti) => {
confettiRef.current = confetti ?? undefined;
}}
particleCount={500}
scalar={1}
shapes={['circle', 'square', 'star']}
spread={360}
startVelocity={45}
ticks={600}
className="position-fixed top-0 left-0 w-screen h-screen pointer-events-none"
/>
);
}
42 changes: 15 additions & 27 deletions src/components/confetti/use-confetti.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
import {useCallback, useMemo} from 'react';
import {useCallback, useMemo, useState} from 'react';
import {type Handler, type Payload} from '../broadcast/use-channel-handlers';
import {type ConfettiReactionEntry} from '../reactions/reaction';

export default function useConfetti({
postMessage,
onConfetti,
onReset,
}: {
postMessage?: Handler;
onConfetti?: (data: any) => void;
onReset?: (data: any) => void;
}): {
export default function useConfetti({postMessage}: {postMessage?: Handler}): {
postConfetti: () => void;
postConfettiReset: () => void;
handlers: Handler[];
confettiReactions: ConfettiReactionEntry[];
} {
const postConfetti = useCallback(() => {
postMessage?.({id: 'reaction', reaction: ['', 'confetti']});
postMessage?.({id: 'reactions', reactions: [['', 'confetti']]});
}, [postMessage]);

const postConfettiReset = useCallback(() => {
postMessage?.({id: 'reaction', reaction: ['', 'confetti clear']});
}, [postMessage]);
const [confettiReactions, setConfettiReactions] = useState<
ConfettiReactionEntry[]
>([]);

const handlers = useMemo<Handler[]>(
() => [
(payload: Payload) => {
if (payload.id === 'reaction' && payload.reaction[1] === 'confetti') {
onConfetti?.({});
}
},
(payload: Payload) => {
if (
payload.id === 'reaction' &&
payload.reaction[1] === 'confetti clear'
) {
onReset?.({});
if (payload.id === 'reactions') {
const confettiReactions = payload.reactions.filter(
([, reaction]) => reaction === 'confetti',
) as ConfettiReactionEntry[];
setConfettiReactions(confettiReactions);
}
},
],
[onConfetti, onReset],
[],
);

return {postConfetti, postConfettiReset, handlers};
return {postConfetti, handlers, confettiReactions};
}
28 changes: 13 additions & 15 deletions src/components/reactions/Reactions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import clsx from 'clsx';
import {useEffect, useMemo, useRef} from 'react';
import {
type IconReaction,
type IconReactionEntry,
type IconReactionMap,
} from './reaction';
import {type IconReaction, type IconReactionMap} from './reaction';
import reactionsIconMap from './reaction-icons-map';

// This file is inspired from these 2 articles:
Expand Down Expand Up @@ -74,20 +70,22 @@ export default function Reactions({
removeReaction,
}: {
reactions: IconReactionMap;
removeReaction: (reaction: IconReactionEntry) => void;
removeReaction: (id: string) => void;
}) {
return (
<div className="fixed top-0 left-0 h-screen w-screen pointer-events-none">
<div className="relative left-[calc(3rem_+_20px)] h-full w-[calc(calc(100vw_-_6rem)_-_40px)] max-h-screen">
{Array.from(reactions.entries()).map(([id, reaction]) => (
<Reaction
key={id}
reaction={reaction}
onReactionDone={() => {
removeReaction([id, reaction]);
}}
/>
))}
{Array.from(reactions.entries())
.filter(([, renderedReaction]) => !renderedReaction.done)
.map(([id, renderedReaction]) => (
<Reaction
key={id}
reaction={renderedReaction.reaction}
onReactionDone={() => {
removeReaction(id);
}}
/>
))}
</div>
</div>
);
Expand Down
16 changes: 12 additions & 4 deletions src/components/reactions/reaction.d.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import {type Timestamp, type FieldValue} from 'firebase/firestore';

export type IconReaction = 'love' | 'smile' | 'applause' | 'explode';
export type ConfettiReaction = 'confetti' | 'confetti clear';
export type ReactionType = IconReaction | ConfettiReaction;
export type ConfettiReaction = 'confetti';
export type ClearReaction = 'clear';
export type ReactionType = IconReaction | ConfettiReaction | ClearReaction;
export type ReactionEntry = [string, ReactionType];
export type IconReactionEntry = [string, IconReaction];
export type IconReactionMap = Map<string, IconReaction>;
export type IconReactionEntry = [string, RenderedReaction];
export type IconReactionMap = Map<string, RenderedReaction>;
export type ConfettiReactionEntry = [string, ConfettiReaction];
export type ClearReactionEntry = [string, ClearReactionEntry];

type CommonReactionData = {
reaction: ReactionType;
ttl: Date;
};

type RenderedReaction = {
done?: boolean;
reaction: IconReaction;
};

export type IncomingReactionData = CommonReactionData & {
created: Timestamp;
};
Expand Down
Loading

0 comments on commit 1111fd6

Please sign in to comment.