Call Reaction Bursts
This commit is contained in:
parent
775c881688
commit
2394a25fc1
8 changed files with 547 additions and 9 deletions
19
stylesheets/components/CallReactionBurst.scss
Normal file
19
stylesheets/components/CallReactionBurst.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CallingReactionsBurstToasts {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
inset-block-end: calc($CallControls__height + 32px);
|
||||
inset-inline-start: 15px;
|
||||
}
|
||||
|
||||
.CallReactionBursts {
|
||||
position: absolute;
|
||||
z-index: $z-index-toast;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.CallReactionBurstEmoji {
|
||||
position: absolute;
|
||||
}
|
|
@ -10,7 +10,10 @@
|
|||
|
||||
// Reactions appear in the same space as the Raised Hands button. When they are both
|
||||
// present then move Reactions up.
|
||||
.CallingRaisedHandsList__Button + .CallingReactionsToasts {
|
||||
.CallingRaisedHandsList__Button + .CallingReactionsToasts,
|
||||
.CallingRaisedHandsList__Button
|
||||
+ .CallingReactionsToasts
|
||||
+ .CallingReactionsBurstToasts {
|
||||
inset-block-end: calc($CallControls__height + 100px);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
@import './components/CallingRaisedHandsList.scss';
|
||||
@import './components/CallingRaisedHandsToasts.scss';
|
||||
@import './components/CallingReactionsToasts.scss';
|
||||
@import './components/CallReactionBurst.scss';
|
||||
@import './components/ChatColorPicker.scss';
|
||||
@import './components/Checkbox.scss';
|
||||
@import './components/CircleCheckbox.scss';
|
||||
|
|
177
ts/components/CallReactionBurst.tsx
Normal file
177
ts/components/CallReactionBurst.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useIsMounted } from '../hooks/useIsMounted';
|
||||
import { CallReactionBurstEmoji } from './CallReactionBurstEmoji';
|
||||
|
||||
const LIFETIME = 3000;
|
||||
|
||||
export type CallReactionBurstType = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
type CallReactionBurstStateType = CallReactionBurstType & {
|
||||
key: string;
|
||||
};
|
||||
|
||||
type CallReactionBurstContextType = {
|
||||
showBurst: (burst: CallReactionBurstType) => string;
|
||||
hideBurst: (key: string) => void;
|
||||
};
|
||||
|
||||
const CallReactionBurstContext =
|
||||
createContext<CallReactionBurstContextType | null>(null);
|
||||
|
||||
export function CallReactionBurstProvider({
|
||||
children,
|
||||
region,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
region?: React.RefObject<HTMLElement>;
|
||||
}): JSX.Element {
|
||||
const [bursts, setBursts] = useState<Array<CallReactionBurstStateType>>([]);
|
||||
const timeouts = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||
const shownBursts = useRef<Set<string>>(new Set());
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const clearBurstTimeout = useCallback((key: string) => {
|
||||
const timeout = timeouts.current.get(key);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeouts.current.delete(key);
|
||||
}, []);
|
||||
|
||||
const hideBurst = useCallback(
|
||||
(key: string) => {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearBurstTimeout(key);
|
||||
|
||||
setBursts(state => {
|
||||
const existingIndex = state.findIndex(burst => burst.key === key);
|
||||
if (existingIndex === -1) {
|
||||
// Important to return the same state object here to avoid infinite recursion if
|
||||
// hideBurst is in a useEffect dependency array
|
||||
return state;
|
||||
}
|
||||
return [
|
||||
...state.slice(0, existingIndex),
|
||||
...state.slice(existingIndex + 1),
|
||||
];
|
||||
});
|
||||
},
|
||||
[isMounted, clearBurstTimeout]
|
||||
);
|
||||
|
||||
const startTimer = useCallback(
|
||||
(key: string, duration: number) => {
|
||||
timeouts.current.set(
|
||||
key,
|
||||
setTimeout(() => hideBurst(key), duration)
|
||||
);
|
||||
},
|
||||
[hideBurst]
|
||||
);
|
||||
|
||||
const showBurst = useCallback(
|
||||
(burst: CallReactionBurstType): string => {
|
||||
const key = uuid();
|
||||
|
||||
setBursts(state => {
|
||||
startTimer(key, LIFETIME);
|
||||
state.unshift({ ...burst, key });
|
||||
shownBursts.current.add(key);
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
return key;
|
||||
},
|
||||
[startTimer]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return {
|
||||
showBurst,
|
||||
hideBurst,
|
||||
};
|
||||
}, [showBurst, hideBurst]);
|
||||
|
||||
// Immediately trigger a state update before the portal gets shown to prevent
|
||||
// DOM jumping on initial render
|
||||
const [container, setContainer] = useState(document.body);
|
||||
React.useLayoutEffect(() => {
|
||||
if (region?.current) {
|
||||
setContainer(region.current);
|
||||
}
|
||||
}, [region]);
|
||||
|
||||
return (
|
||||
<CallReactionBurstContext.Provider value={contextValue}>
|
||||
{createPortal(
|
||||
<div className="CallReactionBursts">
|
||||
{bursts.map(({ value, key }) => (
|
||||
<CallReactionBurstEmoji
|
||||
key={key}
|
||||
value={value}
|
||||
onAnimationEnd={() => hideBurst(key)}
|
||||
/>
|
||||
))}
|
||||
</div>,
|
||||
container
|
||||
)}
|
||||
{children}
|
||||
</CallReactionBurstContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Use this to access showBurst and hideBurst and ensure bursts are hidden on unmount
|
||||
export function useCallReactionBursts(): CallReactionBurstContextType {
|
||||
const context = useContext(CallReactionBurstContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'Call Reaction Bursts must be wrapped in CallReactionBurstProvider'
|
||||
);
|
||||
}
|
||||
const burstsShown = useRef<Set<string>>(new Set());
|
||||
|
||||
const wrappedShowBurst = useCallback(
|
||||
(burst: CallReactionBurstType) => {
|
||||
const key = context.showBurst(burst);
|
||||
burstsShown.current.add(key);
|
||||
return key;
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
const hideAllShownBursts = useCallback(() => {
|
||||
[...burstsShown.current].forEach(context.hideBurst);
|
||||
}, [context]);
|
||||
|
||||
useEffect(() => {
|
||||
return hideAllShownBursts;
|
||||
}, [hideAllShownBursts]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
...context,
|
||||
showBurst: wrappedShowBurst,
|
||||
}),
|
||||
[wrappedShowBurst, context]
|
||||
);
|
||||
}
|
152
ts/components/CallReactionBurstEmoji.tsx
Normal file
152
ts/components/CallReactionBurstEmoji.tsx
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import { random } from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
|
||||
export type PropsType = {
|
||||
value: string;
|
||||
onAnimationEnd?: () => unknown;
|
||||
};
|
||||
|
||||
const NUM_EMOJIS = 6;
|
||||
const DELAY_BETWEEN_EMOJIS = 120;
|
||||
const EMOJI_HEIGHT = 36;
|
||||
|
||||
type AnimationConfig = {
|
||||
mass: number;
|
||||
tension: number;
|
||||
friction: number;
|
||||
clamp: boolean;
|
||||
precision: number;
|
||||
velocity: number;
|
||||
};
|
||||
|
||||
export function CallReactionBurstEmoji({ value }: PropsType): JSX.Element {
|
||||
const [toY, setToY] = React.useState<number>(0);
|
||||
const fromY = -50;
|
||||
|
||||
const generateEmojiProps = React.useCallback(() => {
|
||||
return {
|
||||
key: uuid(),
|
||||
value,
|
||||
springConfig: {
|
||||
mass: random(10, 20),
|
||||
tension: random(60, 90),
|
||||
friction: random(20, 60),
|
||||
clamp: true,
|
||||
precision: 0,
|
||||
velocity: -0.01,
|
||||
},
|
||||
fromY,
|
||||
toY,
|
||||
toScale: random(1, 2.5, true),
|
||||
fromRotate: random(-45, 45),
|
||||
toRotate: random(-45, 45),
|
||||
};
|
||||
}, [fromY, toY, value]);
|
||||
|
||||
// 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.
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
React.useLayoutEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const { top } = containerRef.current.getBoundingClientRect();
|
||||
const calculatedToY = -top;
|
||||
setToY(calculatedToY);
|
||||
setEmojis([{ ...generateEmojiProps(), toY: calculatedToY }]);
|
||||
}
|
||||
}, [generateEmojiProps]);
|
||||
|
||||
const [emojis, setEmojis] = React.useState<Array<AnimatedEmojiProps>>([
|
||||
generateEmojiProps(),
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setEmojis(curEmojis => {
|
||||
const emojiCount = curEmojis.length;
|
||||
if (emojiCount + 1 >= NUM_EMOJIS) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
return [...curEmojis, generateEmojiProps()];
|
||||
});
|
||||
}, DELAY_BETWEEN_EMOJIS);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [fromY, toY, value, generateEmojiProps]);
|
||||
|
||||
return (
|
||||
<div className="CallReactionBurstEmoji" ref={containerRef}>
|
||||
{emojis.map(props => (
|
||||
<AnimatedEmoji {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type AnimatedEmojiProps = {
|
||||
value: string;
|
||||
fromRotate: number;
|
||||
fromY: number;
|
||||
toRotate: number;
|
||||
toScale: number;
|
||||
toY: number;
|
||||
springConfig: AnimationConfig;
|
||||
onAnimationEnd?: () => unknown;
|
||||
};
|
||||
|
||||
export function AnimatedEmoji({
|
||||
value,
|
||||
fromRotate,
|
||||
fromY,
|
||||
toRotate,
|
||||
toScale,
|
||||
toY,
|
||||
springConfig,
|
||||
onAnimationEnd,
|
||||
}: AnimatedEmojiProps): JSX.Element {
|
||||
const height = EMOJI_HEIGHT * toScale;
|
||||
|
||||
const { rotate, y } = useSpring({
|
||||
from: {
|
||||
rotate: fromRotate,
|
||||
y: fromY,
|
||||
},
|
||||
to: {
|
||||
rotate: toRotate,
|
||||
y: toY - height - 10,
|
||||
},
|
||||
config: springConfig,
|
||||
onRest: onAnimationEnd,
|
||||
});
|
||||
|
||||
// These styles animate faster than Y.
|
||||
// Reactions toasts animate with opacity so harmonize with that.
|
||||
const { scale } = useSpring({
|
||||
from: {
|
||||
scale: 0.5,
|
||||
},
|
||||
to: {
|
||||
scale: toScale,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
className="CallReactionBurstEmoji"
|
||||
style={{
|
||||
rotate,
|
||||
scale,
|
||||
y,
|
||||
}}
|
||||
>
|
||||
<Emojify sizeClass="medium" text={value} />
|
||||
</animated.div>
|
||||
);
|
||||
}
|
|
@ -645,7 +645,7 @@ export function GroupCallReactions(): JSX.Element {
|
|||
}
|
||||
|
||||
export function GroupCallReactionsSpam(): JSX.Element {
|
||||
const remoteParticipants = allRemoteParticipants.slice(0, 5);
|
||||
const remoteParticipants = allRemoteParticipants.slice(0, 3);
|
||||
const [props] = React.useState(
|
||||
createProps({
|
||||
callMode: CallMode.Group,
|
||||
|
@ -662,7 +662,7 @@ export function GroupCallReactionsSpam(): JSX.Element {
|
|||
return <CallScreen {...props} activeCall={activeCall} />;
|
||||
}
|
||||
|
||||
export function GroupCallReactionsBurstInOrder(): JSX.Element {
|
||||
export function GroupCallReactionsManyInOrder(): JSX.Element {
|
||||
const timestamp = Date.now();
|
||||
const remoteParticipants = allRemoteParticipants.slice(0, 5);
|
||||
const reactions = remoteParticipants.map((participant, i) => {
|
||||
|
|
|
@ -77,6 +77,11 @@ import type { Props as ReactionPickerProps } from './conversation/ReactionPicker
|
|||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
||||
import { Emoji } from './emoji/Emoji';
|
||||
import { CallingRaisedHandsList } from './CallingRaisedHandsList';
|
||||
import type { CallReactionBurstType } from './CallReactionBurst';
|
||||
import {
|
||||
CallReactionBurstProvider,
|
||||
useCallReactionBursts,
|
||||
} from './CallReactionBurst';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -126,6 +131,20 @@ const REACTIONS_TOASTS_TRANSITION_FROM = {
|
|||
opacity: 0,
|
||||
};
|
||||
|
||||
// How many reactions of the same emoji must occur before a burst.
|
||||
const REACTIONS_BURST_THRESHOLD = 3;
|
||||
|
||||
// Timeframe in which multiple of the same emoji must occur before a burst.
|
||||
const REACTIONS_BURST_WINDOW = 4000;
|
||||
|
||||
// Timeframe after a burst where new reactions of the same emoji are ignored for
|
||||
// bursting. They are considered part of the recent burst.
|
||||
const REACTIONS_BURST_TRAILING_WINDOW = 2000;
|
||||
|
||||
// Max number of bursts in a short timeframe to avoid overwhelming the user.
|
||||
const REACTIONS_BURST_MAX_IN_SHORT_WINDOW = 3;
|
||||
const REACTIONS_BURST_SHORT_WINDOW = 4000;
|
||||
|
||||
function CallDuration({
|
||||
joinedAt,
|
||||
}: {
|
||||
|
@ -996,8 +1015,13 @@ type CallingReactionsToastsType = {
|
|||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
function useReactionsToast(props: CallingReactionsToastsType): void {
|
||||
const { reactions, conversationsByDemuxId, localDemuxId, i18n } = props;
|
||||
type UseReactionsToastType = CallingReactionsToastsType & {
|
||||
showBurst: (toast: CallReactionBurstType) => string;
|
||||
};
|
||||
|
||||
function useReactionsToast(props: UseReactionsToastType): void {
|
||||
const { reactions, conversationsByDemuxId, localDemuxId, i18n, showBurst } =
|
||||
props;
|
||||
const ourServiceId: ServiceIdString | undefined = localDemuxId
|
||||
? conversationsByDemuxId.get(localDemuxId)?.serviceId
|
||||
: undefined;
|
||||
|
@ -1005,6 +1029,13 @@ function useReactionsToast(props: CallingReactionsToastsType): void {
|
|||
const [previousReactions, setPreviousReactions] = React.useState<
|
||||
ActiveCallReactionsType | undefined
|
||||
>(undefined);
|
||||
const reactionsShown = useRef<
|
||||
Map<
|
||||
string,
|
||||
{ value: string; isBursted: boolean; expireAt: number; demuxId: number }
|
||||
>
|
||||
>(new Map());
|
||||
const burstsShown = useRef<Map<string, number>>(new Map());
|
||||
const { showToast } = useCallingToasts();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1016,10 +1047,13 @@ function useReactionsToast(props: CallingReactionsToastsType): void {
|
|||
return;
|
||||
}
|
||||
|
||||
const time = Date.now();
|
||||
let anyReactionWasShown = false;
|
||||
reactions.forEach(({ timestamp, demuxId, value }) => {
|
||||
const conversation = conversationsByDemuxId.get(demuxId);
|
||||
const key = `reactions-${timestamp}-${demuxId}`;
|
||||
showToast({
|
||||
key: `reactions-${timestamp}-${demuxId}`,
|
||||
key,
|
||||
onlyShowOnce: true,
|
||||
autoClose: true,
|
||||
content: (
|
||||
|
@ -1032,10 +1066,100 @@ function useReactionsToast(props: CallingReactionsToastsType): void {
|
|||
</span>
|
||||
),
|
||||
});
|
||||
|
||||
// Track shown reactions for burst purposes. Skip if it's already tracked.
|
||||
if (reactionsShown.current.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a recent burst for this emoji, treat it as part of that burst.
|
||||
const recentBurstTime = burstsShown.current.get(value);
|
||||
const isBursted = !!(
|
||||
recentBurstTime &&
|
||||
recentBurstTime + REACTIONS_BURST_TRAILING_WINDOW > time
|
||||
);
|
||||
reactionsShown.current.set(key, {
|
||||
value,
|
||||
isBursted,
|
||||
expireAt: timestamp + REACTIONS_BURST_WINDOW,
|
||||
demuxId,
|
||||
});
|
||||
anyReactionWasShown = true;
|
||||
});
|
||||
|
||||
if (!anyReactionWasShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unburstedEmojis = new Map<string, Set<string>>();
|
||||
const unburstedEmojisReactorIds = new Map<
|
||||
string,
|
||||
Set<ServiceIdString | number>
|
||||
>();
|
||||
reactionsShown.current.forEach(
|
||||
({ value, isBursted, expireAt, demuxId }, key) => {
|
||||
if (expireAt < time) {
|
||||
reactionsShown.current.delete(key);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBursted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionKeys = unburstedEmojis.get(value) ?? new Set();
|
||||
reactionKeys.add(key);
|
||||
unburstedEmojis.set(value, reactionKeys);
|
||||
|
||||
// Only burst when enough unique people react.
|
||||
const conversation = conversationsByDemuxId.get(demuxId);
|
||||
const reactorId = conversation?.serviceId || demuxId;
|
||||
const reactorIdSet = unburstedEmojisReactorIds.get(value) ?? new Set();
|
||||
reactorIdSet.add(reactorId);
|
||||
unburstedEmojisReactorIds.set(value, reactorIdSet);
|
||||
}
|
||||
);
|
||||
|
||||
burstsShown.current.forEach((timestamp, value) => {
|
||||
if (timestamp < time - REACTIONS_BURST_SHORT_WINDOW) {
|
||||
burstsShown.current.delete(value);
|
||||
}
|
||||
});
|
||||
|
||||
if (burstsShown.current.size >= REACTIONS_BURST_MAX_IN_SHORT_WINDOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [value, reactorIds] of unburstedEmojisReactorIds.entries()) {
|
||||
if (reactorIds.size < REACTIONS_BURST_THRESHOLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reactionKeys = unburstedEmojis.get(value);
|
||||
if (!reactionKeys) {
|
||||
unburstedEmojisReactorIds.delete(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
burstsShown.current.set(value, time);
|
||||
reactionKeys.forEach(key => {
|
||||
const reactionShown = reactionsShown.current.get(key);
|
||||
if (!reactionShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactionShown.isBursted = true;
|
||||
});
|
||||
showBurst({ value });
|
||||
|
||||
if (burstsShown.current.size >= REACTIONS_BURST_MAX_IN_SHORT_WINDOW) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
reactions,
|
||||
previousReactions,
|
||||
showBurst,
|
||||
showToast,
|
||||
conversationsByDemuxId,
|
||||
localDemuxId,
|
||||
|
@ -1049,6 +1173,8 @@ function CallingReactionsToastsContainer(
|
|||
): JSX.Element {
|
||||
const { i18n } = props;
|
||||
const toastRegionRef = useRef<HTMLDivElement>(null);
|
||||
const burstRegionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<CallingToastProvider
|
||||
i18n={i18n}
|
||||
|
@ -1057,14 +1183,18 @@ function CallingReactionsToastsContainer(
|
|||
lifetime={CALLING_REACTIONS_LIFETIME}
|
||||
transitionFrom={REACTIONS_TOASTS_TRANSITION_FROM}
|
||||
>
|
||||
<div className="CallingReactionsToasts" ref={toastRegionRef} />
|
||||
<CallingReactionsToasts {...props} />
|
||||
<CallReactionBurstProvider region={burstRegionRef}>
|
||||
<div className="CallingReactionsToasts" ref={toastRegionRef} />
|
||||
<div className="CallingReactionsBurstToasts" ref={burstRegionRef} />
|
||||
<CallingReactionsToasts {...props} />
|
||||
</CallReactionBurstProvider>
|
||||
</CallingToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function CallingReactionsToasts(props: CallingReactionsToastsType) {
|
||||
useReactionsToast(props);
|
||||
const { showBurst } = useCallReactionBursts();
|
||||
useReactionsToast({ ...props, showBurst });
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -2994,6 +2994,14 @@
|
|||
"updated": "2023-11-14T23:29:51.425Z",
|
||||
"reasonDetail": "Used to detect clicks outside of the Calling More Options button menu and ensures clicking the button does not re-open the menu."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const burstRegionRef = useRef<HTMLDivElement>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2023-12-21T11:13:56.623Z",
|
||||
"reasonDetail": "Calling reactions bursts"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallingLobby.tsx",
|
||||
|
@ -3957,5 +3965,53 @@
|
|||
"line": " message.innerHTML = window.i18n('icu:optimizingApplication');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-17T21:02:59.414Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const timeouts = useRef<Map<string, NodeJS.Timeout>>(new Map());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For hiding call reaction bursts after timeouts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const shownBursts = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Keep track of shown reaction bursts."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurst.tsx",
|
||||
"line": " const burstsShown = useRef<Set<string>>(new Set());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "In wrapping function, track bursts so we can hide on unmount."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallReactionBurstEmoji.tsx",
|
||||
"line": " const containerRef = React.useRef<HTMLDivElement | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "For determining position of container for animations."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const reactionsShown = useRef<",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent reactions shown for reactions burst"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CallScreen.tsx",
|
||||
"line": " const burstsShown = useRef<Map<string, number>>(new Map());",
|
||||
"reasonCategory": "sageTrusted",
|
||||
"updated": "2024-01-06T00:59:20.678Z",
|
||||
"reasonDetail": "Recent bursts shown for burst behavior like throttling."
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Add table
Reference in a new issue