signal-desktop/ts/components/CustomizingPreferredReactionsModal.tsx

198 lines
5.5 KiB
TypeScript

// 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 { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../reactions/constants';
import { convertShortName } from './emoji/lib';
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 selected =
typeof selectedDraftEmojiIndex === 'number'
? draftPreferredReactions[selectedDraftEmojiIndex]
: undefined;
const onPick = isSaving
? noop
: (pickedEmoji: string) => {
selectDraftEmojiToBeReplaced(
draftPreferredReactions.findIndex(emoji => emoji === pickedEmoji)
);
};
const hasChanged = !isEqual(
originalPreferredReactions,
draftPreferredReactions
);
const canReset =
!isSaving &&
!isEqual(
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
convertShortName(shortName, skinTone)
),
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}
onSetSkinTone={shouldNotBeCalled}
ref={setReferenceElement}
preferredReactionEmoji={draftPreferredReactions}
selected={selected}
selectionStyle={ReactionPickerSelectionStyle.Menu}
renderEmojiPicker={shouldNotBeCalled}
/>
{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={pickedEmoji => {
const emoji = convertShortName(
pickedEmoji.shortName,
pickedEmoji.skinTone
);
replaceSelectedDraftEmoji(emoji);
}}
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');
}