2021-09-09 16:29:01 +00:00
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2020-01-23 23:57:37 +00:00
|
|
|
import * as React from 'react';
|
|
|
|
import classNames from 'classnames';
|
2021-09-09 16:29:01 +00:00
|
|
|
import * as log from '../../logging/log';
|
2020-01-23 23:57:37 +00:00
|
|
|
import { Emoji } from '../emoji/Emoji';
|
2020-05-05 19:49:34 +00:00
|
|
|
import { convertShortName } from '../emoji/lib';
|
|
|
|
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
2021-09-09 16:29:01 +00:00
|
|
|
import { missingCaseError } from '../../util/missingCaseError';
|
2021-08-02 21:19:18 +00:00
|
|
|
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
2020-05-05 19:49:34 +00:00
|
|
|
import { LocalizerType } from '../../types/Util';
|
|
|
|
|
2021-09-09 16:29:01 +00:00
|
|
|
export enum ReactionPickerSelectionStyle {
|
|
|
|
Picker,
|
|
|
|
Menu,
|
|
|
|
}
|
|
|
|
|
2020-05-05 19:49:34 +00:00
|
|
|
export type RenderEmojiPickerProps = Pick<Props, 'onClose' | 'style'> &
|
2021-09-09 16:29:01 +00:00
|
|
|
Pick<EmojiPickerProps, 'onClickSettings' | 'onPickEmoji'> & {
|
2020-05-05 19:49:34 +00:00
|
|
|
ref: React.Ref<HTMLDivElement>;
|
|
|
|
};
|
2020-01-23 23:57:37 +00:00
|
|
|
|
|
|
|
export type OwnProps = {
|
2021-09-09 16:29:01 +00:00
|
|
|
hasMoreButton?: boolean;
|
2020-05-05 19:49:34 +00:00
|
|
|
i18n: LocalizerType;
|
2020-01-23 23:57:37 +00:00
|
|
|
selected?: string;
|
2021-09-09 16:29:01 +00:00
|
|
|
selectionStyle: ReactionPickerSelectionStyle;
|
2020-01-23 23:57:37 +00:00
|
|
|
onClose?: () => unknown;
|
|
|
|
onPick: (emoji: string) => unknown;
|
2021-09-09 16:29:01 +00:00
|
|
|
openCustomizePreferredReactionsModal?: () => unknown;
|
|
|
|
preferredReactionEmoji: Array<string>;
|
2020-05-05 19:49:34 +00:00
|
|
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
|
2020-10-02 20:05:09 +00:00
|
|
|
skinTone: number;
|
2020-01-23 23:57:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
|
|
|
|
2021-09-07 21:30:58 +00:00
|
|
|
const EmojiButton = React.forwardRef<
|
|
|
|
HTMLButtonElement,
|
|
|
|
{
|
|
|
|
emoji: string;
|
|
|
|
onSelect: () => unknown;
|
|
|
|
selected: boolean;
|
|
|
|
title?: string;
|
|
|
|
}
|
|
|
|
>(({ emoji, onSelect, selected, title }, ref) => (
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
key={emoji}
|
|
|
|
ref={ref}
|
|
|
|
tabIndex={0}
|
|
|
|
className={classNames(
|
|
|
|
'module-ReactionPicker__button',
|
|
|
|
'module-ReactionPicker__button--emoji',
|
|
|
|
selected && 'module-ReactionPicker__button--selected'
|
|
|
|
)}
|
|
|
|
onClick={e => {
|
|
|
|
e.stopPropagation();
|
|
|
|
onSelect();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Emoji size={48} emoji={emoji} title={title} />
|
|
|
|
</button>
|
|
|
|
));
|
|
|
|
|
2020-01-23 23:57:37 +00:00
|
|
|
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
2020-10-02 20:05:09 +00:00
|
|
|
(
|
2021-09-09 16:29:01 +00:00
|
|
|
{
|
|
|
|
hasMoreButton = true,
|
|
|
|
i18n,
|
|
|
|
onClose,
|
|
|
|
onPick,
|
|
|
|
openCustomizePreferredReactionsModal,
|
|
|
|
preferredReactionEmoji,
|
|
|
|
renderEmojiPicker,
|
|
|
|
selected,
|
|
|
|
selectionStyle,
|
|
|
|
skinTone,
|
|
|
|
style,
|
|
|
|
},
|
2020-10-02 20:05:09 +00:00
|
|
|
ref
|
|
|
|
) => {
|
2020-05-05 19:49:34 +00:00
|
|
|
const [pickingOther, setPickingOther] = React.useState(false);
|
2020-01-23 23:57:37 +00:00
|
|
|
|
|
|
|
// Handle escape key
|
|
|
|
React.useEffect(() => {
|
|
|
|
const handler = (e: KeyboardEvent) => {
|
|
|
|
if (onClose && e.key === 'Escape') {
|
|
|
|
onClose();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handler);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', handler);
|
|
|
|
};
|
|
|
|
}, [onClose]);
|
|
|
|
|
2020-05-05 19:49:34 +00:00
|
|
|
// Handle EmojiPicker::onPickEmoji
|
|
|
|
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(
|
2020-10-02 20:05:09 +00:00
|
|
|
({ shortName, skinTone: pickedSkinTone }) => {
|
|
|
|
onPick(convertShortName(shortName, pickedSkinTone));
|
2020-05-05 19:49:34 +00:00
|
|
|
},
|
|
|
|
[onPick]
|
|
|
|
);
|
|
|
|
|
2021-09-07 21:30:58 +00:00
|
|
|
// Focus first button and restore focus on unmount
|
|
|
|
const [focusRef] = useRestoreFocus();
|
|
|
|
|
|
|
|
if (pickingOther) {
|
2021-09-09 16:29:01 +00:00
|
|
|
return renderEmojiPicker({
|
|
|
|
onClickSettings: openCustomizePreferredReactionsModal,
|
|
|
|
onClose,
|
|
|
|
onPickEmoji,
|
|
|
|
ref,
|
|
|
|
style,
|
|
|
|
});
|
2021-09-07 21:30:58 +00:00
|
|
|
}
|
|
|
|
|
2021-09-09 16:29:01 +00:00
|
|
|
const emojis = preferredReactionEmoji.map(shortName =>
|
2020-10-02 20:05:09 +00:00
|
|
|
convertShortName(shortName, skinTone)
|
|
|
|
);
|
|
|
|
|
2020-06-03 18:36:17 +00:00
|
|
|
const otherSelected = selected && !emojis.includes(selected);
|
2020-05-05 19:49:34 +00:00
|
|
|
|
2021-09-07 21:30:58 +00:00
|
|
|
let moreButton: React.ReactNode;
|
2021-09-09 16:29:01 +00:00
|
|
|
if (!hasMoreButton) {
|
|
|
|
moreButton = undefined;
|
|
|
|
} else if (otherSelected) {
|
2021-09-07 21:30:58 +00:00
|
|
|
moreButton = (
|
|
|
|
<EmojiButton
|
|
|
|
emoji={selected}
|
|
|
|
onSelect={() => {
|
|
|
|
onPick(selected);
|
|
|
|
}}
|
|
|
|
selected
|
|
|
|
title={i18n('Reactions--remove')}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
moreButton = (
|
|
|
|
<button
|
2021-09-08 16:25:16 +00:00
|
|
|
aria-label={i18n('Reactions--more')}
|
2021-09-07 21:30:58 +00:00
|
|
|
className="module-ReactionPicker__button module-ReactionPicker__button--more"
|
|
|
|
onClick={event => {
|
|
|
|
event.stopPropagation();
|
|
|
|
setPickingOther(true);
|
|
|
|
}}
|
|
|
|
tabIndex={0}
|
2021-09-08 16:25:16 +00:00
|
|
|
title={i18n('Reactions--more')}
|
2021-09-07 21:30:58 +00:00
|
|
|
type="button"
|
|
|
|
>
|
|
|
|
<div className="module-ReactionPicker__button--more__dot" />
|
|
|
|
<div className="module-ReactionPicker__button--more__dot" />
|
|
|
|
<div className="module-ReactionPicker__button--more__dot" />
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-09 16:29:01 +00:00
|
|
|
let selectionStyleClassName: string;
|
|
|
|
switch (selectionStyle) {
|
|
|
|
case ReactionPickerSelectionStyle.Picker:
|
|
|
|
selectionStyleClassName = 'module-ReactionPicker--picker-style';
|
|
|
|
break;
|
|
|
|
case ReactionPickerSelectionStyle.Menu:
|
|
|
|
selectionStyleClassName = 'module-ReactionPicker--menu-style';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
log.error(missingCaseError(selectionStyle));
|
|
|
|
selectionStyleClassName = 'module-ReactionPicker--picker-style';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-09-07 21:30:58 +00:00
|
|
|
return (
|
2021-09-09 16:29:01 +00:00
|
|
|
<div
|
|
|
|
ref={ref}
|
|
|
|
style={style}
|
|
|
|
className={classNames(
|
|
|
|
'module-ReactionPicker',
|
|
|
|
selectionStyleClassName,
|
|
|
|
selected ? 'module-ReactionPicker--something-selected' : undefined
|
|
|
|
)}
|
|
|
|
>
|
2020-06-03 18:36:17 +00:00
|
|
|
{emojis.map((emoji, index) => {
|
2020-01-23 23:57:37 +00:00
|
|
|
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
|
|
|
|
|
|
|
return (
|
2021-09-07 21:30:58 +00:00
|
|
|
<EmojiButton
|
|
|
|
emoji={emoji}
|
2020-01-23 23:57:37 +00:00
|
|
|
key={emoji}
|
2021-09-07 21:30:58 +00:00
|
|
|
onSelect={() => {
|
2020-01-23 23:57:37 +00:00
|
|
|
onPick(emoji);
|
|
|
|
}}
|
2021-09-07 21:30:58 +00:00
|
|
|
ref={maybeFocusRef}
|
|
|
|
selected={emoji === selected}
|
|
|
|
/>
|
2020-01-23 23:57:37 +00:00
|
|
|
);
|
|
|
|
})}
|
2021-09-07 21:30:58 +00:00
|
|
|
{moreButton}
|
2020-01-23 23:57:37 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|