Reactions: skin tone support

This commit is contained in:
Sidney Keese 2020-10-02 13:05:09 -07:00 committed by Josh Perez
parent 6a7d45b6fc
commit c3ddedfde1
13 changed files with 307 additions and 141 deletions

View file

@ -2,11 +2,7 @@ import * as React from 'react';
import { Editor } from 'draft-js'; import { Editor } from 'draft-js';
import { get, noop } from 'lodash'; import { get, noop } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { EmojiButton, Props as EmojiButtonProps } from './emoji/EmojiButton';
EmojiButton,
EmojiPickDataType,
Props as EmojiButtonProps,
} from './emoji/EmojiButton';
import { import {
Props as StickerButtonProps, Props as StickerButtonProps,
StickerButton, StickerButton,
@ -22,6 +18,7 @@ import {
} from './conversation/MessageRequestActions'; } from './conversation/MessageRequestActions';
import { countStickers } from './stickers/lib'; import { countStickers } from './stickers/lib';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { EmojiPickDataType } from './emoji/EmojiPicker';
export type OwnProps = { export type OwnProps = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;

View file

@ -20,7 +20,7 @@ import {
OwnProps as ReactionViewerProps, OwnProps as ReactionViewerProps,
ReactionViewer, ReactionViewer,
} from './ReactionViewer'; } from './ReactionViewer';
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker'; import { Props as ReactionPickerProps } from './ReactionPicker';
import { Emoji } from '../emoji/Emoji'; import { Emoji } from '../emoji/Emoji';
import { LinkPreviewDate } from './LinkPreviewDate'; import { LinkPreviewDate } from './LinkPreviewDate';
@ -44,6 +44,8 @@ import { isFileDangerous } from '../../util/isFileDangerous';
import { BodyRangesType, LocalizerType } from '../../types/Util'; import { BodyRangesType, LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors'; import { ColorType } from '../../types/Colors';
import { createRefMerger } from '../_util'; import { createRefMerger } from '../_util';
import { emojiToData } from '../emoji/lib';
import { SmartReactionPicker } from '../../state/smart/ReactionPicker';
interface Trigger { interface Trigger {
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void; handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
@ -1297,8 +1299,7 @@ export class Message extends React.PureComponent<Props, State> {
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
<Popper placement="top"> <Popper placement="top">
{({ ref, style }) => ( {({ ref, style }) => (
<ReactionPicker <SmartReactionPicker
i18n={i18n}
ref={ref} ref={ref}
style={style} style={style}
selected={selectedReaction} selected={selectedReaction}
@ -1726,13 +1727,24 @@ export class Message extends React.PureComponent<Props, State> {
return null; return null;
} }
const reactionsWithEmojiData = reactions.map(reaction => ({
...reaction,
...emojiToData(reaction.emoji),
}));
// Group by emoji and order each group by timestamp descending // Group by emoji and order each group by timestamp descending
const grouped = Object.values(groupBy(reactions, 'emoji')).map(res => const groupedAndSortedReactions = Object.values(
orderBy(res, ['timestamp'], ['desc']) groupBy(reactionsWithEmojiData, 'short_name')
).map(groupedReactions =>
orderBy(
groupedReactions,
[reaction => reaction.from.isMe, 'timestamp'],
['desc', 'desc']
)
); );
// Order groups by length and subsequently by most recent reaction // Order groups by length and subsequently by most recent reaction
const ordered = orderBy( const ordered = orderBy(
grouped, groupedAndSortedReactions,
['length', ([{ timestamp }]) => timestamp], ['length', ([{ timestamp }]) => timestamp],
['desc', 'desc'] ['desc', 'desc']
); );

View file

@ -3,6 +3,7 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { select } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker'; import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
@ -32,6 +33,7 @@ storiesOf('Components/Conversation/ReactionPicker', module)
i18n={i18n} i18n={i18n}
onPick={action('onPick')} onPick={action('onPick')}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
skinTone={0}
/> />
); );
}) })
@ -43,6 +45,24 @@ storiesOf('Components/Conversation/ReactionPicker', module)
selected={e} selected={e}
onPick={action('onPick')} onPick={action('onPick')}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
skinTone={0}
/>
</div>
));
})
.add('Skin Tones', () => {
return ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
<div key={e} style={{ height: '100px' }}>
<ReactionPicker
i18n={i18n}
selected={e}
onPick={action('onPick')}
renderEmojiPicker={renderEmojiPicker}
skinTone={select(
'skinTone',
{ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 },
0
)}
/> />
</div> </div>
)); ));

View file

@ -17,14 +17,25 @@ export type OwnProps = {
onClose?: () => unknown; onClose?: () => unknown;
onPick: (emoji: string) => unknown; onPick: (emoji: string) => unknown;
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement; renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
skinTone: number;
}; };
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>; export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
const emojis = ['❤️', '👍', '👎', '😂', '😮', '😢']; const DEFAULT_EMOJI_LIST = [
'heart',
'thumbsup',
'thumbsdown',
'joy',
'open_mouth',
'cry',
];
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>( export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
({ i18n, selected, onClose, onPick, renderEmojiPicker, style }, ref) => { (
{ i18n, selected, onClose, skinTone, onPick, renderEmojiPicker, style },
ref
) => {
const [pickingOther, setPickingOther] = React.useState(false); const [pickingOther, setPickingOther] = React.useState(false);
const focusRef = React.useRef<HTMLButtonElement>(null); const focusRef = React.useRef<HTMLButtonElement>(null);
@ -45,12 +56,16 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
// Handle EmojiPicker::onPickEmoji // Handle EmojiPicker::onPickEmoji
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback( const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(
({ shortName, skinTone }) => { ({ shortName, skinTone: pickedSkinTone }) => {
onPick(convertShortName(shortName, skinTone)); onPick(convertShortName(shortName, pickedSkinTone));
}, },
[onPick] [onPick]
); );
const emojis = DEFAULT_EMOJI_LIST.map(shortName =>
convertShortName(shortName, skinTone)
);
// Focus first button and restore focus on unmount // Focus first button and restore focus on unmount
useRestoreFocus(focusRef); useRestoreFocus(focusRef);

View file

@ -176,3 +176,42 @@ story.add('Picked Missing Reaction', () => {
}); });
return <ReactionViewer {...props} />; return <ReactionViewer {...props} />;
}); });
const skinTones = [
'\u{1F3FB}',
'\u{1F3FC}',
'\u{1F3FD}',
'\u{1F3FE}',
'\u{1F3FF}',
];
const thumbsUpHands = skinTones.map(skinTone => `👍${skinTone}`);
const okHands = skinTones.map(skinTone => `👌${skinTone}`).reverse();
const createReaction = (
emoji: string,
name: string,
timestamp = Date.now()
) => ({
emoji,
from: {
id: '+14155552671',
name,
title: name,
},
timestamp,
});
story.add('Reaction Skin Tones', () => {
const props = createProps({
pickedReaction: '😡',
reactions: [
...thumbsUpHands.map((emoji, n) =>
createReaction(emoji, `Thumbs Up ${n + 1}`, Date.now() + n * 1000)
),
...okHands.map((emoji, n) =>
createReaction(emoji, `Ok Hand ${n + 1}`, Date.now() + n * 1000)
),
],
});
return <ReactionViewer {...props} />;
});

View file

@ -1,11 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { groupBy, mapValues, orderBy, sortBy } from 'lodash'; import { groupBy, mapValues, orderBy } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Avatar, Props as AvatarProps } from '../Avatar'; import { Avatar, Props as AvatarProps } from '../Avatar';
import { Emoji } from '../emoji/Emoji'; import { Emoji } from '../emoji/Emoji';
import { useRestoreFocus } from '../../util/hooks'; import { useRestoreFocus } from '../../util/hooks';
import { ColorType } from '../../types/Colors'; import { ColorType } from '../../types/Colors';
import { emojiToData, EmojiData } from '../emoji/lib';
export type Reaction = { export type Reaction = {
emoji: string; emoji: string;
@ -32,14 +33,91 @@ export type Props = OwnProps &
Pick<React.HTMLProps<HTMLDivElement>, 'style'> & Pick<React.HTMLProps<HTMLDivElement>, 'style'> &
Pick<AvatarProps, 'i18n'>; Pick<AvatarProps, 'i18n'>;
const emojisOrder = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡']; const DEFAULT_EMOJI_ORDER = [
'heart',
'+1',
'-1',
'joy',
'open_mouth',
'cry',
'rage',
];
interface ReactionCategory {
count: number;
emoji?: string;
id: string;
index: number;
}
type ReactionWithEmojiData = Reaction & EmojiData;
export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>( export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => { ({ i18n, reactions, onClose, pickedReaction, ...rest }, ref) => {
const grouped = mapValues(groupBy(reactions, 'emoji'), res => const reactionsWithEmojiData = React.useMemo(
orderBy(res, ['timestamp'], ['desc']) () =>
reactions
.map(reaction => {
const emojiData = emojiToData(reaction.emoji);
if (!emojiData) {
return undefined;
}
return {
...reaction,
...emojiData,
};
})
.filter(
(
reactionWithEmojiData
): reactionWithEmojiData is ReactionWithEmojiData =>
Boolean(reactionWithEmojiData)
),
[reactions]
); );
const [selected, setSelected] = React.useState(pickedReaction || 'all');
const groupedAndSortedReactions = React.useMemo(
() =>
mapValues(
{
all: reactionsWithEmojiData,
...groupBy(reactionsWithEmojiData, 'short_name'),
},
groupedReactions => orderBy(groupedReactions, ['timestamp'], ['desc'])
),
[reactionsWithEmojiData]
);
const reactionCategories: Array<ReactionCategory> = React.useMemo(
() =>
[
{
id: 'all',
index: 0,
count: reactionsWithEmojiData.length,
},
...Object.entries(groupedAndSortedReactions)
.filter(([key]) => key !== 'all')
.map(([, [{ short_name: id, emoji }, ...otherReactions]]) => {
return {
id,
index: DEFAULT_EMOJI_ORDER.includes(id)
? DEFAULT_EMOJI_ORDER.indexOf(id)
: Infinity,
emoji,
count: otherReactions.length + 1,
};
}),
].sort((a, b) => a.index - b.index),
[reactionsWithEmojiData, groupedAndSortedReactions]
);
const [
selectedReactionCategory,
setSelectedReactionCategory,
] = React.useState(pickedReaction || 'all');
const focusRef = React.useRef<HTMLButtonElement>(null); const focusRef = React.useRef<HTMLButtonElement>(null);
// Handle escape key // Handle escape key
@ -60,87 +138,68 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
// Focus first button and restore focus on unmount // Focus first button and restore focus on unmount
useRestoreFocus(focusRef); useRestoreFocus(focusRef);
// Create sorted reaction categories, supporting reaction types we don't
// explicitly know about yet
const renderedEmojis = React.useMemo(() => {
const emojiSet = new Set<string>();
reactions.forEach(re => emojiSet.add(re.emoji));
const arr = sortBy(Array.from(emojiSet), emoji => {
const idx = emojisOrder.indexOf(emoji);
if (idx > -1) {
return idx;
}
return Infinity;
});
return ['all', ...arr];
}, [reactions]);
const allSorted = React.useMemo(() => {
return orderBy(reactions, ['timestamp'], ['desc']);
}, [reactions]);
// If we have previously selected a reaction type that is no longer present // If we have previously selected a reaction type that is no longer present
// (removed on another device, for instance) we should select another // (removed on another device, for instance) we should select another
// reaction type // reaction type
React.useEffect(() => { React.useEffect(() => {
if (!grouped[selected]) { if (
const toSelect = renderedEmojis[0]; !reactionCategories.find(({ id }) => id === selectedReactionCategory)
if (toSelect) { ) {
setSelected(toSelect); if (reactionsWithEmojiData.length > 0) {
setSelectedReactionCategory('all');
} else if (onClose) { } else if (onClose) {
// We have nothing to render!
onClose(); onClose();
} }
} }
}, [grouped, onClose, renderedEmojis, selected, setSelected]); }, [
reactionCategories,
onClose,
reactionsWithEmojiData,
selectedReactionCategory,
]);
const selectedReactions = grouped[selected] || allSorted; const selectedReactions =
groupedAndSortedReactions[selectedReactionCategory] || [];
return ( return (
<div {...rest} ref={ref} className="module-reaction-viewer"> <div {...rest} ref={ref} className="module-reaction-viewer">
<header className="module-reaction-viewer__header"> <header className="module-reaction-viewer__header">
{renderedEmojis {reactionCategories.map(({ id, emoji, count }, index) => {
.filter(e => e === 'all' || Boolean(grouped[e])) const isAll = index === 0;
.map((cat, index) => { const maybeFocusRef = isAll ? focusRef : undefined;
const re = grouped[cat] || reactions;
const maybeFocusRef = index === 0 ? focusRef : undefined;
const isAll = cat === 'all';
return ( return (
<button <button
type="button" type="button"
key={cat} key={id}
ref={maybeFocusRef} ref={maybeFocusRef}
className={classNames( className={classNames(
'module-reaction-viewer__header__button', 'module-reaction-viewer__header__button',
selected === cat selectedReactionCategory === id
? 'module-reaction-viewer__header__button--selected' ? 'module-reaction-viewer__header__button--selected'
: null : null
)} )}
onClick={event => { onClick={event => {
event.stopPropagation(); event.stopPropagation();
setSelected(cat); setSelectedReactionCategory(id);
}} }}
> >
{isAll ? ( {isAll ? (
<span className="module-reaction-viewer__header__button__all"> <span className="module-reaction-viewer__header__button__all">
{i18n('ReactionsViewer--all')}&thinsp;&middot;&thinsp; {i18n('ReactionsViewer--all')}&thinsp;&middot;&thinsp;
{re.length} {count}
</span>
) : (
<>
<Emoji size={18} emoji={emoji} />
<span className="module-reaction-viewer__header__button__count">
{count}
</span> </span>
) : ( </>
<> )}
<Emoji size={18} emoji={cat} /> </button>
<span className="module-reaction-viewer__header__button__count"> );
{re.length} })}
</span>
</>
)}
</button>
);
})}
</header> </header>
<main className="module-reaction-viewer__body"> <main className="module-reaction-viewer__body">
{selectedReactions.map(({ from, emoji }) => ( {selectedReactions.map(({ from, emoji }) => (

View file

@ -3,15 +3,9 @@ import classNames from 'classnames';
import { get, noop } from 'lodash'; import { get, noop } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { import { EmojiPicker, Props as EmojiPickerProps } from './EmojiPicker';
EmojiPickDataType,
EmojiPicker,
Props as EmojiPickerProps,
} from './EmojiPicker';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
export { EmojiPickDataType };
export type OwnProps = { export type OwnProps = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
}; };

View file

@ -24,7 +24,6 @@ export type EmojiPickDataType = { skinTone?: number; shortName: string };
export type OwnProps = { export type OwnProps = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly disableSkinTones?: boolean;
readonly onPickEmoji: (o: EmojiPickDataType) => unknown; readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
readonly doSend?: () => unknown; readonly doSend?: () => unknown;
readonly skinTone?: number; readonly skinTone?: number;
@ -63,7 +62,6 @@ export const EmojiPicker = React.memo(
doSend, doSend,
onPickEmoji, onPickEmoji,
skinTone = 0, skinTone = 0,
disableSkinTones = false,
onSetSkinTone, onSetSkinTone,
recentEmojis = [], recentEmojis = [],
style, style,
@ -79,9 +77,7 @@ export const EmojiPicker = React.memo(
const [searchMode, setSearchMode] = React.useState(false); const [searchMode, setSearchMode] = React.useState(false);
const [searchText, setSearchText] = React.useState(''); const [searchText, setSearchText] = React.useState('');
const [scrollToRow, setScrollToRow] = React.useState(0); const [scrollToRow, setScrollToRow] = React.useState(0);
const [selectedTone, setSelectedTone] = React.useState( const [selectedTone, setSelectedTone] = React.useState(skinTone);
disableSkinTones ? 0 : skinTone
);
const handleToggleSearch = React.useCallback( const handleToggleSearch = React.useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
@ -383,28 +379,26 @@ export const EmojiPicker = React.memo(
/> />
</div> </div>
)} )}
{!disableSkinTones ? ( <footer className="module-emoji-picker__footer">
<footer className="module-emoji-picker__footer"> {[0, 1, 2, 3, 4, 5].map(tone => (
{[0, 1, 2, 3, 4, 5].map(tone => ( <button
<button type="button"
type="button" key={tone}
key={tone} data-tone={tone}
data-tone={tone} onClick={handlePickTone}
onClick={handlePickTone} title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
title={i18n('EmojiPicker--skin-tone', [`${tone}`])} className={classNames(
className={classNames( 'module-emoji-picker__button',
'module-emoji-picker__button', 'module-emoji-picker__button--footer',
'module-emoji-picker__button--footer', selectedTone === tone
selectedTone === tone ? 'module-emoji-picker__button--selected'
? 'module-emoji-picker__button--selected' : null
: null )}
)} >
> <Emoji shortName="hand" skinTone={tone} size={20} />
<Emoji shortName="hand" skinTone={tone} size={20} /> </button>
</button> ))}
))} </footer>
</footer>
) : null}
</div> </div>
); );
} }

View file

@ -263,9 +263,21 @@ export function convertShortName(
} }
export function emojiToImage(emoji: string): string | undefined { export function emojiToImage(emoji: string): string | undefined {
if (!Object.prototype.hasOwnProperty.call(imageByEmoji, emoji)) {
return undefined;
}
return imageByEmoji[emoji]; return imageByEmoji[emoji];
} }
export function emojiToData(emoji: string): EmojiData | undefined {
if (!Object.prototype.hasOwnProperty.call(dataByEmoji, emoji)) {
return undefined;
}
return dataByEmoji[emoji];
}
function getCountOfAllMatches(str: string, regex: RegExp) { function getCountOfAllMatches(str: string, regex: RegExp) {
let match = regex.exec(str); let match = regex.exec(str);
let count = 0; let count = 0;

View file

@ -14,11 +14,8 @@ import { LocalizerType } from '../../types/Util';
export const SmartEmojiPicker = React.forwardRef< export const SmartEmojiPicker = React.forwardRef<
HTMLDivElement, HTMLDivElement,
Pick< Pick<EmojiPickerProps, 'onPickEmoji' | 'onClose' | 'style'>
EmojiPickerProps, >(({ onPickEmoji, onClose, style }, ref) => {
'onPickEmoji' | 'onClose' | 'style' | 'disableSkinTones'
>
>(({ onPickEmoji, onClose, style, disableSkinTones }, ref) => {
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const skinTone = useSelector<StateType, number>(state => const skinTone = useSelector<StateType, number>(state =>
get(state, ['items', 'skinTone'], 0) get(state, ['items', 'skinTone'], 0)
@ -55,7 +52,6 @@ export const SmartEmojiPicker = React.forwardRef<
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
onClose={onClose} onClose={onClose}
style={style} style={style}
disableSkinTones={disableSkinTones}
/> />
); );
}); });

View file

@ -0,0 +1,29 @@
import * as React from 'react';
import { useSelector } from 'react-redux';
import { get } from 'lodash';
import { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { LocalizerType } from '../../types/Util';
import {
ReactionPicker,
Props,
} from '../../components/conversation/ReactionPicker';
type ExternalProps = Omit<Props, 'skinTone' | 'i18n'>;
export const SmartReactionPicker = React.forwardRef<
HTMLDivElement,
ExternalProps
>((props, ref) => {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const skinTone = useSelector<StateType, number>(state =>
get(state, ['items', 'skinTone'], 0)
);
return (
<ReactionPicker ref={ref} skinTone={skinTone} i18n={i18n} {...props} />
);
});

View file

@ -63,7 +63,6 @@ function renderEmojiPicker({
onPickEmoji={onPickEmoji} onPickEmoji={onPickEmoji}
onClose={onClose} onClose={onClose}
style={style} style={style}
disableSkinTones
/> />
); );
} }

View file

@ -12907,7 +12907,7 @@
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.tsx", "path": "ts/components/CompositionArea.tsx",
"line": " el.innerHTML = '';", "line": " el.innerHTML = '';",
"lineNumber": 81, "lineNumber": 78,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-06-03T19:23:21.195Z", "updated": "2020-06-03T19:23:21.195Z",
"reasonDetail": "Our code, no user input, only clearing out the dom" "reasonDetail": "Our code, no user input, only clearing out the dom"
@ -13057,7 +13057,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.audioRef = react_1.default.createRef();", "line": " this.audioRef = react_1.default.createRef();",
"lineNumber": 59, "lineNumber": 60,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z" "updated": "2020-08-28T16:12:19.904Z"
}, },
@ -13065,7 +13065,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.focusRef = react_1.default.createRef();", "line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 60, "lineNumber": 61,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-11T17:24:56.124Z", "updated": "2020-09-11T17:24:56.124Z",
"reasonDetail": "Used for managing focus only" "reasonDetail": "Used for managing focus only"
@ -13074,7 +13074,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.js", "path": "ts/components/conversation/Message.js",
"line": " this.reactionsContainerRef = react_1.default.createRef();", "line": " this.reactionsContainerRef = react_1.default.createRef();",
"lineNumber": 61, "lineNumber": 62,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z", "updated": "2020-08-28T16:12:19.904Z",
"reasonDetail": "Used for detecting clicks outside reaction viewer" "reasonDetail": "Used for detecting clicks outside reaction viewer"
@ -13083,23 +13083,23 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 215,
"reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 217, "lineNumber": 217,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z" "updated": "2020-09-08T20:19:01.913Z"
}, },
{
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 219,
"reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z"
},
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 221, "lineNumber": 223,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-08-28T19:36:40.817Z" "updated": "2020-08-28T19:36:40.817Z"
}, },
@ -13344,4 +13344,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z" "updated": "2020-09-08T23:07:22.682Z"
} }
] ]