Preferred reactions: store raw emoji, gate on feature flag
This commit is contained in:
parent
9b45b3dae2
commit
e2392433e0
16 changed files with 168 additions and 192 deletions
|
@ -8,6 +8,7 @@ import type { WebAPIType } from './textsecure/WebAPI';
|
|||
export type ConfigKeyType =
|
||||
| 'desktop.announcementGroup'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.customizePreferredReactions'
|
||||
| 'desktop.disableGV1'
|
||||
| 'desktop.groupCallOutboundRing'
|
||||
| 'desktop.groupCalling'
|
||||
|
|
|
@ -23,26 +23,12 @@ const defaultProps: ComponentProps<
|
|||
'cancelCustomizePreferredReactionsModal'
|
||||
),
|
||||
deselectDraftEmoji: action('deselectDraftEmoji'),
|
||||
draftPreferredReactions: [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'thumbsup',
|
||||
],
|
||||
draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
|
||||
hadSaveError: false,
|
||||
i18n,
|
||||
isSaving: false,
|
||||
onSetSkinTone: action('onSetSkinTone'),
|
||||
originalPreferredReactions: [
|
||||
'heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
],
|
||||
originalPreferredReactions: ['❤️', '👍', '👎', '😂', '😮', '😢'],
|
||||
replaceSelectedDraftEmoji: action('replaceSelectedDraftEmoji'),
|
||||
resetDraftEmoji: action('resetDraftEmoji'),
|
||||
savePreferredReactions: action('savePreferredReactions'),
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
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 { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants';
|
||||
import { offsetDistanceModifier } from '../util/popperUtil';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -94,20 +94,16 @@ export function CustomizingPreferredReactionsModal({
|
|||
};
|
||||
}, [isSomethingSelected, popperElement, deselectDraftEmoji]);
|
||||
|
||||
const emojis = draftPreferredReactions.map(shortName =>
|
||||
convertShortName(shortName, skinTone)
|
||||
);
|
||||
|
||||
const selected =
|
||||
typeof selectedDraftEmojiIndex === 'number'
|
||||
? emojis[selectedDraftEmojiIndex]
|
||||
? draftPreferredReactions[selectedDraftEmojiIndex]
|
||||
: undefined;
|
||||
|
||||
const onPick = isSaving
|
||||
? noop
|
||||
: (pickedEmoji: string) => {
|
||||
selectDraftEmojiToBeReplaced(
|
||||
emojis.findIndex(emoji => emoji === pickedEmoji)
|
||||
draftPreferredReactions.findIndex(emoji => emoji === pickedEmoji)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -117,7 +113,12 @@ export function CustomizingPreferredReactionsModal({
|
|||
);
|
||||
const canReset =
|
||||
!isSaving &&
|
||||
!isEqual(DEFAULT_PREFERRED_REACTION_EMOJI, draftPreferredReactions);
|
||||
!isEqual(
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
|
||||
convertShortName(shortName, skinTone)
|
||||
),
|
||||
draftPreferredReactions
|
||||
);
|
||||
const canSave = !isSaving && hasChanged;
|
||||
|
||||
return (
|
||||
|
@ -140,7 +141,6 @@ export function CustomizingPreferredReactionsModal({
|
|||
selected={selected}
|
||||
selectionStyle={ReactionPickerSelectionStyle.Menu}
|
||||
renderEmojiPicker={shouldNotBeCalled}
|
||||
skinTone={skinTone}
|
||||
/>
|
||||
{hadSaveError
|
||||
? i18n('CustomizingPreferredReactions__had-save-error')
|
||||
|
@ -155,8 +155,12 @@ export function CustomizingPreferredReactionsModal({
|
|||
>
|
||||
<EmojiPicker
|
||||
i18n={i18n}
|
||||
onPickEmoji={({ shortName }) => {
|
||||
replaceSelectedDraftEmoji(shortName);
|
||||
onPickEmoji={pickedEmoji => {
|
||||
const emoji = convertShortName(
|
||||
pickedEmoji.shortName,
|
||||
pickedEmoji.skinTone
|
||||
);
|
||||
replaceSelectedDraftEmoji(emoji);
|
||||
}}
|
||||
skinTone={skinTone}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
|
|
|
@ -6,7 +6,6 @@ import * as React from 'react';
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
|
@ -18,14 +17,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker';
|
|||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const preferredReactionEmoji = [
|
||||
'heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
];
|
||||
const preferredReactionEmoji = ['❤️', '👍', '👎', '😂', '😮', '😢'];
|
||||
|
||||
const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
|
||||
onClose,
|
||||
|
@ -56,7 +48,6 @@ storiesOf('Components/Conversation/ReactionPicker', module)
|
|||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
selectionStyle={ReactionPickerSelectionStyle.Picker}
|
||||
skinTone={0}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -74,30 +65,6 @@ storiesOf('Components/Conversation/ReactionPicker', module)
|
|||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
selectionStyle={ReactionPickerSelectionStyle.Picker}
|
||||
skinTone={0}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
})
|
||||
.add('Skin Tones', () => {
|
||||
return ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
||||
<div key={e} style={{ height: '100px' }}>
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
selected={e}
|
||||
onPick={action('onPick')}
|
||||
onSetSkinTone={action('onSetSkinTone')}
|
||||
openCustomizePreferredReactionsModal={action(
|
||||
'openCustomizePreferredReactionsModal'
|
||||
)}
|
||||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
selectionStyle={ReactionPickerSelectionStyle.Picker}
|
||||
skinTone={select(
|
||||
'skinTone',
|
||||
{ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 },
|
||||
5
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { canCustomizePreferredReactions } from '../../util/canCustomizePreferredReactions';
|
||||
|
||||
export enum ReactionPickerSelectionStyle {
|
||||
Picker,
|
||||
|
@ -35,7 +36,6 @@ export type OwnProps = {
|
|||
openCustomizePreferredReactionsModal?: () => unknown;
|
||||
preferredReactionEmoji: Array<string>;
|
||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
|
||||
skinTone: number;
|
||||
};
|
||||
|
||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||
|
@ -81,7 +81,6 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
renderEmojiPicker,
|
||||
selected,
|
||||
selectionStyle,
|
||||
skinTone,
|
||||
style,
|
||||
},
|
||||
ref
|
||||
|
@ -116,7 +115,9 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
|
||||
if (pickingOther) {
|
||||
return renderEmojiPicker({
|
||||
onClickSettings: openCustomizePreferredReactionsModal,
|
||||
onClickSettings: canCustomizePreferredReactions()
|
||||
? openCustomizePreferredReactionsModal
|
||||
: undefined,
|
||||
onClose,
|
||||
onPickEmoji,
|
||||
onSetSkinTone,
|
||||
|
@ -125,11 +126,8 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
});
|
||||
}
|
||||
|
||||
const emojis = preferredReactionEmoji.map(shortName =>
|
||||
convertShortName(shortName, skinTone)
|
||||
);
|
||||
|
||||
const otherSelected = selected && !emojis.includes(selected);
|
||||
const otherSelected =
|
||||
selected && !preferredReactionEmoji.includes(selected);
|
||||
|
||||
let moreButton: React.ReactNode;
|
||||
if (!hasMoreButton) {
|
||||
|
@ -189,7 +187,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
selected ? 'module-ReactionPicker--something-selected' : undefined
|
||||
)}
|
||||
>
|
||||
{emojis.map((emoji, index) => {
|
||||
{preferredReactionEmoji.map((emoji, index) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export const DEFAULT_PREFERRED_REACTION_EMOJI = [
|
||||
export const DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES = [
|
||||
'heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from './constants';
|
||||
import * as emoji from '../components/emoji/lib';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from './constants';
|
||||
import { convertShortName } from '../components/emoji/lib';
|
||||
import { isValidReactionEmoji } from './isValidReactionEmoji';
|
||||
|
||||
const PREFERRED_REACTION_EMOJI_COUNT = DEFAULT_PREFERRED_REACTION_EMOJI.length;
|
||||
const PREFERRED_REACTION_EMOJI_COUNT =
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.length;
|
||||
|
||||
export function getPreferredReactionEmoji(storedValue: unknown): Array<string> {
|
||||
export function getPreferredReactionEmoji(
|
||||
storedValue: unknown,
|
||||
skinTone: number
|
||||
): Array<string> {
|
||||
const isStoredValueValid =
|
||||
Array.isArray(storedValue) &&
|
||||
storedValue.length === PREFERRED_REACTION_EMOJI_COUNT &&
|
||||
storedValue.every(emoji.isShortName) &&
|
||||
storedValue.every(isValidReactionEmoji) &&
|
||||
!hasDuplicates(storedValue);
|
||||
return isStoredValueValid ? storedValue : DEFAULT_PREFERRED_REACTION_EMOJI;
|
||||
return isStoredValueValid
|
||||
? storedValue
|
||||
: DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
|
||||
convertShortName(shortName, skinTone)
|
||||
);
|
||||
}
|
||||
|
||||
function hasDuplicates(arr: ReadonlyArray<unknown>): boolean {
|
||||
|
|
|
@ -8,8 +8,10 @@ import * as Errors from '../../types/errors';
|
|||
import { replaceIndex } from '../../util/replaceIndex';
|
||||
import { useBoundActions } from '../../util/hooks';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../reactions/constants';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants';
|
||||
import { getPreferredReactionEmoji } from '../../reactions/getPreferredReactionEmoji';
|
||||
import { getEmojiSkinTone } from '../selectors/items';
|
||||
import { convertShortName } from '../../components/emoji/lib';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -61,7 +63,10 @@ type ReplaceSelectedDraftEmojiActionType = {
|
|||
payload: string;
|
||||
};
|
||||
|
||||
type ResetDraftEmojiActionType = { type: typeof RESET_DRAFT_EMOJI };
|
||||
type ResetDraftEmojiActionType = {
|
||||
type: typeof RESET_DRAFT_EMOJI;
|
||||
payload: { skinTone: number };
|
||||
};
|
||||
|
||||
type SavePreferredReactionsFulfilledActionType = {
|
||||
type: typeof SAVE_PREFERRED_REACTIONS_FULFILLED;
|
||||
|
@ -109,8 +114,10 @@ function openCustomizePreferredReactionsModal(): ThunkAction<
|
|||
OpenCustomizePreferredReactionsModalActionType
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const originalPreferredReactions = getPreferredReactionEmoji(
|
||||
getState().items.preferredReactionEmoji
|
||||
getState().items.preferredReactionEmoji,
|
||||
getEmojiSkinTone(state)
|
||||
);
|
||||
dispatch({
|
||||
type: OPEN_CUSTOMIZE_PREFERRED_REACTIONS_MODAL,
|
||||
|
@ -128,8 +135,16 @@ function replaceSelectedDraftEmoji(
|
|||
};
|
||||
}
|
||||
|
||||
function resetDraftEmoji(): ResetDraftEmojiActionType {
|
||||
return { type: RESET_DRAFT_EMOJI };
|
||||
function resetDraftEmoji(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
ResetDraftEmojiActionType
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const skinTone = getEmojiSkinTone(getState());
|
||||
dispatch({ type: RESET_DRAFT_EMOJI, payload: { skinTone } });
|
||||
};
|
||||
}
|
||||
|
||||
function savePreferredReactions(): ThunkAction<
|
||||
|
@ -253,7 +268,8 @@ export function reducer(
|
|||
},
|
||||
};
|
||||
}
|
||||
case RESET_DRAFT_EMOJI:
|
||||
case RESET_DRAFT_EMOJI: {
|
||||
const { skinTone } = action.payload;
|
||||
if (!state.customizePreferredReactionsModal) {
|
||||
return state;
|
||||
}
|
||||
|
@ -261,10 +277,13 @@ export function reducer(
|
|||
...state,
|
||||
customizePreferredReactionsModal: {
|
||||
...state.customizePreferredReactionsModal,
|
||||
draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(
|
||||
shortName => convertShortName(shortName, skinTone)
|
||||
),
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
case SAVE_PREFERRED_REACTIONS_PENDING:
|
||||
if (!state.customizePreferredReactionsModal) {
|
||||
return state;
|
||||
|
|
|
@ -65,6 +65,10 @@ export const getEmojiSkinTone = createSelector(
|
|||
|
||||
export const getPreferredReactionEmoji = createSelector(
|
||||
getItems,
|
||||
(state: Readonly<ItemsStateType>): Array<string> =>
|
||||
getPreferredReactionEmojiFromStoredValue(state.preferredReactionEmoji)
|
||||
getEmojiSkinTone,
|
||||
(state: Readonly<ItemsStateType>, skinTone: number): Array<string> =>
|
||||
getPreferredReactionEmojiFromStoredValue(
|
||||
state.preferredReactionEmoji,
|
||||
skinTone
|
||||
)
|
||||
);
|
||||
|
|
|
@ -8,10 +8,7 @@ import { useActions as usePreferredReactionsActions } from '../ducks/preferredRe
|
|||
import { useActions as useItemsActions } from '../ducks/items';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getEmojiSkinTone,
|
||||
getPreferredReactionEmoji,
|
||||
} from '../selectors/items';
|
||||
import { getPreferredReactionEmoji } from '../selectors/items';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
|
@ -45,10 +42,6 @@ export const SmartReactionPicker = React.forwardRef<
|
|||
getPreferredReactionEmoji
|
||||
);
|
||||
|
||||
const skinTone = useSelector<StateType, number>(state =>
|
||||
getEmojiSkinTone(state)
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactionPicker
|
||||
i18n={i18n}
|
||||
|
@ -59,7 +52,6 @@ export const SmartReactionPicker = React.forwardRef<
|
|||
preferredReactionEmoji={preferredReactionEmoji}
|
||||
ref={ref}
|
||||
selectionStyle={ReactionPickerSelectionStyle.Picker}
|
||||
skinTone={skinTone}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../reactions/constants';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants';
|
||||
|
||||
import { getPreferredReactionEmoji } from '../../reactions/getPreferredReactionEmoji';
|
||||
|
||||
|
@ -12,35 +12,34 @@ describe('getPreferredReactionEmoji', () => {
|
|||
// Invalid types
|
||||
undefined,
|
||||
null,
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI.join(','),
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.join(','),
|
||||
// Invalid lengths
|
||||
[],
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI.slice(0, 3),
|
||||
[...DEFAULT_PREFERRED_REACTION_EMOJI, 'sparkles'],
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.slice(0, 3),
|
||||
[...DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES, '✨'],
|
||||
// Non-strings in the array
|
||||
['heart', 'thumbsdown', undefined, 'joy', 'open_mouth', 'cry'],
|
||||
['heart', 'thumbsdown', 99, 'joy', 'open_mouth', 'cry'],
|
||||
['❤️', '👍', undefined, '😂', '😮', '😢'],
|
||||
['❤️', '👍', 99, '😂', '😮', '😢'],
|
||||
// Invalid emoji
|
||||
['heart', 'thumbsdown', 'gorbage!!', 'joy', 'open_mouth', 'cry'],
|
||||
['❤️', '👍', 'x', '😂', '😮', '😢'],
|
||||
['❤️', '👍', 'garbage!!', '😂', '😮', '😢'],
|
||||
['❤️', '👍', '✨✨', '😂', '😮', '😢'],
|
||||
// Has duplicates
|
||||
['heart', 'thumbsdown', 'joy', 'joy', 'open_mouth', 'cry'],
|
||||
['❤️', '👍', '👍', '😂', '😮', '😢'],
|
||||
].forEach(input => {
|
||||
assert.deepStrictEqual(
|
||||
getPreferredReactionEmoji(input),
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI
|
||||
);
|
||||
assert.deepStrictEqual(getPreferredReactionEmoji(input, 2), [
|
||||
'❤️',
|
||||
'👍🏼',
|
||||
'👎🏼',
|
||||
'😂',
|
||||
'😮',
|
||||
'😢',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a custom set if passed a valid value', () => {
|
||||
const input = [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'parking',
|
||||
];
|
||||
assert.deepStrictEqual(getPreferredReactionEmoji(input), input);
|
||||
const input = ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'];
|
||||
assert.deepStrictEqual(getPreferredReactionEmoji(input, 3), input);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,6 +26,8 @@ describe('isValidReactionEmoji', () => {
|
|||
|
||||
it('returns true for strings that are exactly 1 emoji', () => {
|
||||
assert.isTrue(isValidReactionEmoji('🇺🇸'));
|
||||
assert.isTrue(isValidReactionEmoji('👍'));
|
||||
assert.isTrue(isValidReactionEmoji('👍🏾'));
|
||||
assert.isTrue(isValidReactionEmoji('👩❤️👩'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { reducer as rootReducer } from '../../../state/reducer';
|
||||
import { StateType, reducer as rootReducer } from '../../../state/reducer';
|
||||
import { noopAction } from '../../../state/ducks/noop';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../../reactions/constants';
|
||||
|
||||
import {
|
||||
PreferredReactionsStateType,
|
||||
|
@ -15,9 +14,12 @@ import {
|
|||
} from '../../../state/ducks/preferredReactions';
|
||||
|
||||
describe('preferred reactions duck', () => {
|
||||
const getEmptyRootState = () => rootReducer(undefined, noopAction());
|
||||
const getEmptyRootState = (): StateType =>
|
||||
rootReducer(undefined, noopAction());
|
||||
|
||||
const getRootState = (preferredReactions: PreferredReactionsStateType) => ({
|
||||
const getRootState = (
|
||||
preferredReactions: PreferredReactionsStateType
|
||||
): StateType => ({
|
||||
...getEmptyRootState(),
|
||||
preferredReactions,
|
||||
});
|
||||
|
@ -25,22 +27,8 @@ describe('preferred reactions duck', () => {
|
|||
const stateWithOpenCustomizationModal = {
|
||||
...getInitialState(),
|
||||
customizePreferredReactionsModal: {
|
||||
draftPreferredReactions: [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'parking',
|
||||
],
|
||||
originalPreferredReactions: [
|
||||
'blue_heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
],
|
||||
draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
|
||||
originalPreferredReactions: ['💙', '👍', '👎', '😂', '😮', '😢'],
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
isSaving: false as const,
|
||||
hadSaveError: false,
|
||||
|
@ -120,15 +108,26 @@ describe('preferred reactions duck', () => {
|
|||
const { openCustomizePreferredReactionsModal } = actions;
|
||||
|
||||
it('opens the customization modal with defaults if no value was stored', () => {
|
||||
const emptyRootState = getEmptyRootState();
|
||||
const rootState = {
|
||||
...emptyRootState,
|
||||
items: {
|
||||
...emptyRootState.items,
|
||||
skinTone: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = sinon.spy();
|
||||
openCustomizePreferredReactionsModal()(dispatch, getEmptyRootState, null);
|
||||
openCustomizePreferredReactionsModal()(dispatch, () => rootState, null);
|
||||
const [action] = dispatch.getCall(0).args;
|
||||
|
||||
const result = reducer(getEmptyRootState().preferredReactions, action);
|
||||
const result = reducer(rootState.preferredReactions, action);
|
||||
|
||||
const expectedEmoji = ['❤️', '👍🏿', '👎🏿', '😂', '😮', '😢'];
|
||||
|
||||
assert.deepEqual(result.customizePreferredReactionsModal, {
|
||||
draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
originalPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI,
|
||||
draftPreferredReactions: expectedEmoji,
|
||||
originalPreferredReactions: expectedEmoji,
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
isSaving: false,
|
||||
hadSaveError: false,
|
||||
|
@ -136,14 +135,7 @@ describe('preferred reactions duck', () => {
|
|||
});
|
||||
|
||||
it('opens the customization modal with stored values', () => {
|
||||
const storedPreferredReactionEmoji = [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'parking',
|
||||
];
|
||||
const storedPreferredReactionEmoji = ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'];
|
||||
|
||||
const emptyRootState = getEmptyRootState();
|
||||
const state = {
|
||||
|
@ -175,21 +167,21 @@ describe('preferred reactions duck', () => {
|
|||
|
||||
it('is a no-op if the customization modal is not open', () => {
|
||||
const state = getInitialState();
|
||||
const action = replaceSelectedDraftEmoji('cat');
|
||||
const action = replaceSelectedDraftEmoji('🦈');
|
||||
const result = reducer(state, action);
|
||||
|
||||
assert.strictEqual(result, state);
|
||||
});
|
||||
|
||||
it('is a no-op if no emoji is selected', () => {
|
||||
const action = replaceSelectedDraftEmoji('cat');
|
||||
const action = replaceSelectedDraftEmoji('💅');
|
||||
const result = reducer(stateWithOpenCustomizationModal, action);
|
||||
|
||||
assert.strictEqual(result, stateWithOpenCustomizationModal);
|
||||
});
|
||||
|
||||
it('is a no-op if the new emoji is already in the list', () => {
|
||||
const action = replaceSelectedDraftEmoji('shark');
|
||||
const action = replaceSelectedDraftEmoji('✨');
|
||||
const result = reducer(
|
||||
stateWithOpenCustomizationModalAndSelectedEmoji,
|
||||
action
|
||||
|
@ -202,7 +194,7 @@ describe('preferred reactions duck', () => {
|
|||
});
|
||||
|
||||
it('replaces the selected draft emoji and deselects', () => {
|
||||
const action = replaceSelectedDraftEmoji('cat');
|
||||
const action = replaceSelectedDraftEmoji('🐱');
|
||||
const result = reducer(
|
||||
stateWithOpenCustomizationModalAndSelectedEmoji,
|
||||
action
|
||||
|
@ -210,7 +202,7 @@ describe('preferred reactions duck', () => {
|
|||
|
||||
assert.deepStrictEqual(
|
||||
result.customizePreferredReactionsModal?.draftPreferredReactions,
|
||||
['sparkles', 'cat', 'sparkler', 'shark', 'sparkling_heart', 'parking']
|
||||
['✨', '🐱', '🎇', '🦈', '💖', '🅿️']
|
||||
);
|
||||
assert.isUndefined(
|
||||
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
|
||||
|
@ -221,30 +213,39 @@ describe('preferred reactions duck', () => {
|
|||
describe('resetDraftEmoji', () => {
|
||||
const { resetDraftEmoji } = actions;
|
||||
|
||||
function getAction(rootState: Readonly<StateType>) {
|
||||
const dispatch = sinon.spy();
|
||||
resetDraftEmoji()(dispatch, () => rootState, null);
|
||||
const [action] = dispatch.getCall(0).args;
|
||||
return action;
|
||||
}
|
||||
|
||||
it('is a no-op if the customization modal is not open', () => {
|
||||
const state = getInitialState();
|
||||
const action = resetDraftEmoji();
|
||||
const rootState = getEmptyRootState();
|
||||
const state = rootState.preferredReactions;
|
||||
const action = getAction(rootState);
|
||||
const result = reducer(state, action);
|
||||
|
||||
assert.strictEqual(result, state);
|
||||
});
|
||||
|
||||
it('resets the draft emoji to the defaults', () => {
|
||||
const action = resetDraftEmoji();
|
||||
const result = reducer(stateWithOpenCustomizationModal, action);
|
||||
const rootState = getRootState(stateWithOpenCustomizationModal);
|
||||
const action = getAction(rootState);
|
||||
const result = reducer(rootState.preferredReactions, action);
|
||||
|
||||
assert.deepEqual(
|
||||
result.customizePreferredReactionsModal?.draftPreferredReactions,
|
||||
DEFAULT_PREFERRED_REACTION_EMOJI
|
||||
['❤️', '👍', '👎', '😂', '😮', '😢']
|
||||
);
|
||||
});
|
||||
|
||||
it('deselects any selected emoji', () => {
|
||||
const action = resetDraftEmoji();
|
||||
const result = reducer(
|
||||
stateWithOpenCustomizationModalAndSelectedEmoji,
|
||||
action
|
||||
const rootState = getRootState(
|
||||
stateWithOpenCustomizationModalAndSelectedEmoji
|
||||
);
|
||||
const action = getAction(rootState);
|
||||
const result = reducer(rootState.preferredReactions, action);
|
||||
|
||||
assert.isUndefined(
|
||||
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from '../../../state/selectors/items';
|
||||
import type { StateType } from '../../../state/reducer';
|
||||
import type { ItemsStateType } from '../../../state/ducks/items';
|
||||
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../../reactions/constants';
|
||||
|
||||
describe('both/state/selectors/items', () => {
|
||||
// Note: we would like to use the full reducer here, to get a real empty state object
|
||||
|
@ -74,30 +73,28 @@ describe('both/state/selectors/items', () => {
|
|||
describe('#getPreferredReactionEmoji', () => {
|
||||
// See also: the tests for the `getPreferredReactionEmoji` helper.
|
||||
|
||||
const expectedDefault = ['❤️', '👍🏿', '👎🏿', '😂', '😮', '😢'];
|
||||
|
||||
it('returns the default set if no value is stored', () => {
|
||||
const state = getRootState({});
|
||||
const state = getRootState({ skinTone: 5 });
|
||||
const actual = getPreferredReactionEmoji(state);
|
||||
|
||||
assert.deepStrictEqual(actual, DEFAULT_PREFERRED_REACTION_EMOJI);
|
||||
assert.deepStrictEqual(actual, expectedDefault);
|
||||
});
|
||||
|
||||
it('returns the default set if the stored value is invalid', () => {
|
||||
const state = getRootState({ preferredReactionEmoji: ['garbage!!'] });
|
||||
const state = getRootState({
|
||||
skinTone: 5,
|
||||
preferredReactionEmoji: ['garbage!!'],
|
||||
});
|
||||
const actual = getPreferredReactionEmoji(state);
|
||||
|
||||
assert.deepStrictEqual(actual, DEFAULT_PREFERRED_REACTION_EMOJI);
|
||||
assert.deepStrictEqual(actual, expectedDefault);
|
||||
});
|
||||
|
||||
it('returns a custom set of emoji', () => {
|
||||
const preferredReactionEmoji = [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'parking',
|
||||
];
|
||||
const state = getRootState({ preferredReactionEmoji });
|
||||
const preferredReactionEmoji = ['✨', '❇️', '🤙🏻', '🦈', '💖', '🅿️'];
|
||||
const state = getRootState({ skinTone: 5, preferredReactionEmoji });
|
||||
const actual = getPreferredReactionEmoji(state);
|
||||
|
||||
assert.deepStrictEqual(actual, preferredReactionEmoji);
|
||||
|
|
|
@ -28,22 +28,8 @@ describe('both/state/selectors/preferredReactions', () => {
|
|||
getIsCustomizingPreferredReactions(
|
||||
getRootState({
|
||||
customizePreferredReactionsModal: {
|
||||
draftPreferredReactions: [
|
||||
'sparkles',
|
||||
'sparkle',
|
||||
'sparkler',
|
||||
'shark',
|
||||
'sparkling_heart',
|
||||
'parking',
|
||||
],
|
||||
originalPreferredReactions: [
|
||||
'blue_heart',
|
||||
'thumbsup',
|
||||
'thumbsdown',
|
||||
'joy',
|
||||
'open_mouth',
|
||||
'cry',
|
||||
],
|
||||
draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
|
||||
originalPreferredReactions: ['💙', '👍', '👎', '😂', '😮', '😢'],
|
||||
selectedDraftEmojiIndex: undefined,
|
||||
isSaving: false as const,
|
||||
hadSaveError: false,
|
||||
|
|
11
ts/util/canCustomizePreferredReactions.ts
Normal file
11
ts/util/canCustomizePreferredReactions.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
|
||||
export function canCustomizePreferredReactions(): boolean {
|
||||
return Boolean(
|
||||
RemoteConfig.isEnabled('desktop.internalUser') ||
|
||||
RemoteConfig.isEnabled('desktop.customizePreferredReactions')
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue