2021-09-09 16:29:01 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
|
|
import { usePopper } from 'react-popper';
|
|
|
|
import { isEqual, noop } from 'lodash';
|
|
|
|
|
|
|
|
import type { LocalizerType } from '../types/Util';
|
|
|
|
import { Modal } from './Modal';
|
|
|
|
import { Button, ButtonVariant } from './Button';
|
|
|
|
import {
|
|
|
|
ReactionPicker,
|
|
|
|
ReactionPickerSelectionStyle,
|
|
|
|
} from './conversation/ReactionPicker';
|
|
|
|
import { EmojiPicker } from './emoji/EmojiPicker';
|
|
|
|
import { convertShortName } from './emoji/lib';
|
|
|
|
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants';
|
|
|
|
import { offsetDistanceModifier } from '../util/popperUtil';
|
|
|
|
|
|
|
|
type PropsType = {
|
|
|
|
draftPreferredReactions: Array<string>;
|
|
|
|
hadSaveError: boolean;
|
|
|
|
i18n: LocalizerType;
|
|
|
|
isSaving: boolean;
|
|
|
|
originalPreferredReactions: Array<string>;
|
|
|
|
selectedDraftEmojiIndex: undefined | number;
|
|
|
|
skinTone: number;
|
|
|
|
|
|
|
|
cancelCustomizePreferredReactionsModal(): unknown;
|
|
|
|
deselectDraftEmoji(): unknown;
|
|
|
|
onSetSkinTone(tone: number): unknown;
|
|
|
|
replaceSelectedDraftEmoji(newEmoji: string): unknown;
|
|
|
|
resetDraftEmoji(): unknown;
|
|
|
|
savePreferredReactions(): unknown;
|
|
|
|
selectDraftEmojiToBeReplaced(index: number): unknown;
|
|
|
|
};
|
|
|
|
|
|
|
|
export function CustomizingPreferredReactionsModal({
|
|
|
|
cancelCustomizePreferredReactionsModal,
|
|
|
|
deselectDraftEmoji,
|
|
|
|
draftPreferredReactions,
|
|
|
|
hadSaveError,
|
|
|
|
i18n,
|
|
|
|
isSaving,
|
|
|
|
onSetSkinTone,
|
|
|
|
originalPreferredReactions,
|
|
|
|
replaceSelectedDraftEmoji,
|
|
|
|
resetDraftEmoji,
|
|
|
|
savePreferredReactions,
|
|
|
|
selectDraftEmojiToBeReplaced,
|
|
|
|
selectedDraftEmojiIndex,
|
|
|
|
skinTone,
|
|
|
|
}: Readonly<PropsType>): JSX.Element {
|
|
|
|
const [
|
|
|
|
referenceElement,
|
|
|
|
setReferenceElement,
|
|
|
|
] = useState<null | HTMLDivElement>(null);
|
|
|
|
const [popperElement, setPopperElement] = useState<null | HTMLDivElement>(
|
|
|
|
null
|
|
|
|
);
|
|
|
|
const emojiPickerPopper = usePopper(referenceElement, popperElement, {
|
|
|
|
placement: 'bottom',
|
|
|
|
modifiers: [
|
|
|
|
offsetDistanceModifier(8),
|
|
|
|
{
|
|
|
|
name: 'preventOverflow',
|
|
|
|
options: { altAxis: true },
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const isSomethingSelected = selectedDraftEmojiIndex !== undefined;
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!isSomethingSelected) {
|
|
|
|
return noop;
|
|
|
|
}
|
|
|
|
|
|
|
|
const onBodyClick = (event: MouseEvent) => {
|
|
|
|
const { target } = event;
|
|
|
|
if (!(target instanceof HTMLElement) || !popperElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isClickOutsidePicker = !popperElement.contains(target);
|
|
|
|
if (isClickOutsidePicker) {
|
|
|
|
deselectDraftEmoji();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
document.body.addEventListener('click', onBodyClick);
|
|
|
|
return () => {
|
|
|
|
document.body.removeEventListener('click', onBodyClick);
|
|
|
|
};
|
|
|
|
}, [isSomethingSelected, popperElement, deselectDraftEmoji]);
|
|
|
|
|
|
|
|
const emojis = draftPreferredReactions.map(shortName =>
|
|
|
|
convertShortName(shortName, skinTone)
|
|
|
|
);
|
|
|
|
|
|
|
|
const selected =
|
|
|
|
typeof selectedDraftEmojiIndex === 'number'
|
|
|
|
? emojis[selectedDraftEmojiIndex]
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
const onPick = isSaving
|
|
|
|
? noop
|
|
|
|
: (pickedEmoji: string) => {
|
|
|
|
selectDraftEmojiToBeReplaced(
|
|
|
|
emojis.findIndex(emoji => emoji === pickedEmoji)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const hasChanged = !isEqual(
|
|
|
|
originalPreferredReactions,
|
|
|
|
draftPreferredReactions
|
|
|
|
);
|
|
|
|
const canReset =
|
|
|
|
!isSaving &&
|
|
|
|
!isEqual(DEFAULT_PREFERRED_REACTION_EMOJI, draftPreferredReactions);
|
|
|
|
const canSave = !isSaving && hasChanged;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Modal
|
|
|
|
hasXButton
|
|
|
|
i18n={i18n}
|
|
|
|
onClose={() => {
|
|
|
|
cancelCustomizePreferredReactionsModal();
|
|
|
|
}}
|
|
|
|
title={i18n('CustomizingPreferredReactions__title')}
|
|
|
|
>
|
|
|
|
<div className="module-CustomizingPreferredReactionsModal__reaction-picker-wrapper">
|
|
|
|
<ReactionPicker
|
|
|
|
hasMoreButton={false}
|
|
|
|
i18n={i18n}
|
|
|
|
onPick={onPick}
|
2021-09-09 20:53:26 +00:00
|
|
|
onSetSkinTone={shouldNotBeCalled}
|
2021-09-09 16:29:01 +00:00
|
|
|
ref={setReferenceElement}
|
|
|
|
preferredReactionEmoji={draftPreferredReactions}
|
|
|
|
selected={selected}
|
|
|
|
selectionStyle={ReactionPickerSelectionStyle.Menu}
|
|
|
|
renderEmojiPicker={shouldNotBeCalled}
|
|
|
|
skinTone={skinTone}
|
|
|
|
/>
|
|
|
|
{hadSaveError
|
|
|
|
? i18n('CustomizingPreferredReactions__had-save-error')
|
|
|
|
: i18n('CustomizingPreferredReactions__subtitle')}
|
|
|
|
</div>
|
|
|
|
{isSomethingSelected && (
|
|
|
|
<div
|
|
|
|
className="module-CustomizingPreferredReactionsModal__emoji-picker-wrapper"
|
|
|
|
ref={setPopperElement}
|
|
|
|
style={emojiPickerPopper.styles.popper}
|
|
|
|
{...emojiPickerPopper.attributes.popper}
|
|
|
|
>
|
|
|
|
<EmojiPicker
|
|
|
|
i18n={i18n}
|
|
|
|
onPickEmoji={({ shortName }) => {
|
|
|
|
replaceSelectedDraftEmoji(shortName);
|
|
|
|
}}
|
|
|
|
skinTone={skinTone}
|
|
|
|
onSetSkinTone={onSetSkinTone}
|
|
|
|
onClose={() => {
|
|
|
|
deselectDraftEmoji();
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
<Modal.ButtonFooter>
|
|
|
|
<Button
|
|
|
|
disabled={!canReset}
|
|
|
|
onClick={() => {
|
|
|
|
resetDraftEmoji();
|
|
|
|
}}
|
|
|
|
variant={ButtonVariant.SecondaryAffirmative}
|
|
|
|
>
|
|
|
|
{i18n('reset')}
|
|
|
|
</Button>
|
|
|
|
<Button
|
|
|
|
disabled={!canSave}
|
|
|
|
onClick={() => {
|
|
|
|
savePreferredReactions();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{i18n('save')}
|
|
|
|
</Button>
|
|
|
|
</Modal.ButtonFooter>
|
|
|
|
</Modal>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function shouldNotBeCalled(): React.ReactElement {
|
|
|
|
throw new Error('This should not be called');
|
|
|
|
}
|