diff --git a/ts/components/CallReactionBurst.tsx b/ts/components/CallReactionBurst.tsx index b00c21d42359..8e2bb3f54a9a 100644 --- a/ts/components/CallReactionBurst.tsx +++ b/ts/components/CallReactionBurst.tsx @@ -18,7 +18,7 @@ import { CallReactionBurstEmoji } from './CallReactionBurstEmoji'; const LIFETIME = 3000; export type CallReactionBurstType = { - value: string; + values: Array; }; type CallReactionBurstStateType = CallReactionBurstType & { @@ -124,10 +124,10 @@ export function CallReactionBurstProvider({ {createPortal(
- {bursts.map(({ value, key }) => ( + {bursts.map(({ values, key }) => ( hideBurst(key)} /> ))} diff --git a/ts/components/CallReactionBurstEmoji.tsx b/ts/components/CallReactionBurstEmoji.tsx index 2c49dc0f9b4b..47805bf0ab3d 100644 --- a/ts/components/CallReactionBurstEmoji.tsx +++ b/ts/components/CallReactionBurstEmoji.tsx @@ -8,7 +8,7 @@ import { v4 as uuid } from 'uuid'; import { Emojify } from './conversation/Emojify'; export type PropsType = { - value: string; + values: Array; onAnimationEnd?: () => unknown; }; @@ -25,31 +25,36 @@ type AnimationConfig = { velocity: number; }; -export function CallReactionBurstEmoji({ value }: PropsType): JSX.Element { +// values is an array of emojis, which is useful when bursting multi skin tone set of +// emojis to get the correct representation +export function CallReactionBurstEmoji({ values }: PropsType): JSX.Element { const [toY, setToY] = React.useState(0); const fromY = -50; - const generateEmojiProps = React.useCallback(() => { - return { - key: uuid(), - value, - springConfig: { - mass: random(10, 20), - tension: random(45, 60), - friction: random(20, 60), - clamp: true, - precision: 0, - velocity: -0.01, - }, - fromX: random(0, 20), - toX: random(-30, 300), - fromY, - toY, - toScale: random(1, 2.5, true), - fromRotate: random(-45, 45), - toRotate: random(-45, 45), - }; - }, [fromY, toY, value]); + const generateEmojiProps = React.useCallback( + (index: number) => { + return { + key: uuid(), + value: values[index % values.length], + springConfig: { + mass: random(10, 20), + tension: random(45, 60), + friction: random(20, 60), + clamp: true, + precision: 0, + velocity: -0.01, + }, + fromX: random(0, 20), + toX: random(-30, 300), + fromY, + toY, + toScale: random(1, 2.5, true), + fromRotate: random(-45, 45), + toRotate: random(-45, 45), + }; + }, + [fromY, toY, values] + ); // Calculate target Y position before first render. Emojis need to animate Y upwards // by the value of the container's top, plus the emoji's maximum height. @@ -59,12 +64,12 @@ export function CallReactionBurstEmoji({ value }: PropsType): JSX.Element { const { top } = containerRef.current.getBoundingClientRect(); const calculatedToY = -top; setToY(calculatedToY); - setEmojis([{ ...generateEmojiProps(), toY: calculatedToY }]); + setEmojis([{ ...generateEmojiProps(0), toY: calculatedToY }]); } }, [generateEmojiProps]); const [emojis, setEmojis] = React.useState>([ - generateEmojiProps(), + generateEmojiProps(0), ]); React.useEffect(() => { @@ -74,14 +79,14 @@ export function CallReactionBurstEmoji({ value }: PropsType): JSX.Element { if (emojiCount + 1 >= NUM_EMOJIS) { clearInterval(timer); } - return [...curEmojis, generateEmojiProps()]; + return [...curEmojis, generateEmojiProps(emojiCount)]; }); }, DELAY_BETWEEN_EMOJIS); return () => { clearInterval(timer); }; - }, [fromY, toY, value, generateEmojiProps]); + }, [fromY, toY, values, generateEmojiProps]); return (
diff --git a/ts/components/CallScreen.stories.tsx b/ts/components/CallScreen.stories.tsx index 72386d1e04c4..77ec6f19bee9 100644 --- a/ts/components/CallScreen.stories.tsx +++ b/ts/components/CallScreen.stories.tsx @@ -653,9 +653,9 @@ export function GroupCallReactions(): JSX.Element { }) ); - const activeCall = useReactionsEmitter( - props.activeCall as ActiveGroupCallType - ); + const activeCall = useReactionsEmitter({ + activeCall: props.activeCall as ActiveGroupCallType, + }); return ; } @@ -670,11 +670,30 @@ export function GroupCallReactionsSpam(): JSX.Element { }) ); - const activeCall = useReactionsEmitter( - props.activeCall as ActiveGroupCallType, - 250 + const activeCall = useReactionsEmitter({ + activeCall: props.activeCall as ActiveGroupCallType, + frequency: 250, + }); + + return ; +} + +export function GroupCallReactionsSkinTones(): JSX.Element { + const remoteParticipants = allRemoteParticipants.slice(0, 3); + const [props] = React.useState( + createProps({ + callMode: CallMode.Group, + remoteParticipants, + viewMode: CallViewMode.Overflow, + }) ); + const activeCall = useReactionsEmitter({ + activeCall: props.activeCall as ActiveGroupCallType, + frequency: 500, + emojis: ['👍', '👍🏻', '👍🏼', '👍🏽', '👍🏾', '👍🏿', '❤️', '😂', '😮', '😢'], + }); + return ; } @@ -701,11 +720,17 @@ export function GroupCallReactionsManyInOrder(): JSX.Element { return ; } -function useReactionsEmitter( - activeCall: ActiveGroupCallType, +function useReactionsEmitter({ + activeCall, frequency = 2000, - removeAfter = 5000 -) { + removeAfter = 5000, + emojis = DEFAULT_PREFERRED_REACTION_EMOJI, +}: { + activeCall: ActiveGroupCallType; + frequency?: number; + removeAfter?: number; + emojis?: Array; +}) { const [call, setCall] = React.useState(activeCall); React.useEffect(() => { const interval = setInterval(() => { @@ -725,7 +750,7 @@ function useReactionsEmitter( { timestamp: timeNow, demuxId, - value: sample(DEFAULT_PREFERRED_REACTION_EMOJI) as string, + value: sample(emojis) as string, }, ]; @@ -736,7 +761,7 @@ function useReactionsEmitter( }); }, frequency); return () => clearInterval(interval); - }, [frequency, removeAfter, call]); + }, [emojis, frequency, removeAfter, call]); return call; } diff --git a/ts/components/CallScreen.tsx b/ts/components/CallScreen.tsx index 7718f514f3f5..be00d27543c1 100644 --- a/ts/components/CallScreen.tsx +++ b/ts/components/CallScreen.tsx @@ -87,6 +87,7 @@ import { } from './CallReactionBurst'; import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall'; import { assertDev } from '../util/assert'; +import { emojiToData } from './emoji/lib'; export type PropsType = { activeCall: ActiveCallType; @@ -1048,7 +1049,13 @@ function useReactionsToast(props: UseReactionsToastType): void { const reactionsShown = useRef< Map< string, - { value: string; isBursted: boolean; expireAt: number; demuxId: number } + { + value: string; + originalValue: string; + isBursted: boolean; + expireAt: number; + demuxId: number; + } > >(new Map()); const burstsShown = useRef>(new Map()); @@ -1094,8 +1101,13 @@ function useReactionsToast(props: UseReactionsToastType): void { recentBurstTime && recentBurstTime + REACTIONS_BURST_TRAILING_WINDOW > time ); + // Normalize skin tone emoji to calculate burst threshold, but save original + // value to show in the burst animation + const emojiData = emojiToData(value); + const normalizedValue = emojiData?.unified ?? value; reactionsShown.current.set(key, { - value, + value: normalizedValue, + originalValue: value, isBursted, expireAt: timestamp + REACTIONS_BURST_WINDOW, demuxId, @@ -1158,6 +1170,7 @@ function useReactionsToast(props: UseReactionsToastType): void { } burstsShown.current.set(value, time); + const values: Array = []; reactionKeys.forEach(key => { const reactionShown = reactionsShown.current.get(key); if (!reactionShown) { @@ -1165,8 +1178,9 @@ function useReactionsToast(props: UseReactionsToastType): void { } reactionShown.isBursted = true; + values.push(reactionShown.originalValue); }); - showBurst({ value }); + showBurst({ values }); if (burstsShown.current.size >= REACTIONS_BURST_MAX_IN_SHORT_WINDOW) { break;