Let users customize the preferred reaction palette
This commit is contained in:
parent
7a5385e00a
commit
f28456c160
38 changed files with 1788 additions and 124 deletions
193
ts/components/CustomizingPreferredReactionsModal.tsx
Normal file
193
ts/components/CustomizingPreferredReactionsModal.tsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
// 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}
|
||||
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');
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue