Preferred reactions: store raw emoji, gate on feature flag

This commit is contained in:
Evan Hahn 2021-09-09 18:47:30 -05:00 committed by GitHub
parent 9b45b3dae2
commit e2392433e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 168 additions and 192 deletions

View file

@ -8,6 +8,7 @@ import type { WebAPIType } from './textsecure/WebAPI';
export type ConfigKeyType = export type ConfigKeyType =
| 'desktop.announcementGroup' | 'desktop.announcementGroup'
| 'desktop.clientExpiration' | 'desktop.clientExpiration'
| 'desktop.customizePreferredReactions'
| 'desktop.disableGV1' | 'desktop.disableGV1'
| 'desktop.groupCallOutboundRing' | 'desktop.groupCallOutboundRing'
| 'desktop.groupCalling' | 'desktop.groupCalling'

View file

@ -23,26 +23,12 @@ const defaultProps: ComponentProps<
'cancelCustomizePreferredReactionsModal' 'cancelCustomizePreferredReactionsModal'
), ),
deselectDraftEmoji: action('deselectDraftEmoji'), deselectDraftEmoji: action('deselectDraftEmoji'),
draftPreferredReactions: [ draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
'sparkles',
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'thumbsup',
],
hadSaveError: false, hadSaveError: false,
i18n, i18n,
isSaving: false, isSaving: false,
onSetSkinTone: action('onSetSkinTone'), onSetSkinTone: action('onSetSkinTone'),
originalPreferredReactions: [ originalPreferredReactions: ['❤️', '👍', '👎', '😂', '😮', '😢'],
'heart',
'thumbsup',
'thumbsdown',
'joy',
'open_mouth',
'cry',
],
replaceSelectedDraftEmoji: action('replaceSelectedDraftEmoji'), replaceSelectedDraftEmoji: action('replaceSelectedDraftEmoji'),
resetDraftEmoji: action('resetDraftEmoji'), resetDraftEmoji: action('resetDraftEmoji'),
savePreferredReactions: action('savePreferredReactions'), savePreferredReactions: action('savePreferredReactions'),

View file

@ -13,8 +13,8 @@ import {
ReactionPickerSelectionStyle, ReactionPickerSelectionStyle,
} from './conversation/ReactionPicker'; } from './conversation/ReactionPicker';
import { EmojiPicker } from './emoji/EmojiPicker'; import { EmojiPicker } from './emoji/EmojiPicker';
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../reactions/constants';
import { convertShortName } from './emoji/lib'; import { convertShortName } from './emoji/lib';
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../reactions/constants';
import { offsetDistanceModifier } from '../util/popperUtil'; import { offsetDistanceModifier } from '../util/popperUtil';
type PropsType = { type PropsType = {
@ -94,20 +94,16 @@ export function CustomizingPreferredReactionsModal({
}; };
}, [isSomethingSelected, popperElement, deselectDraftEmoji]); }, [isSomethingSelected, popperElement, deselectDraftEmoji]);
const emojis = draftPreferredReactions.map(shortName =>
convertShortName(shortName, skinTone)
);
const selected = const selected =
typeof selectedDraftEmojiIndex === 'number' typeof selectedDraftEmojiIndex === 'number'
? emojis[selectedDraftEmojiIndex] ? draftPreferredReactions[selectedDraftEmojiIndex]
: undefined; : undefined;
const onPick = isSaving const onPick = isSaving
? noop ? noop
: (pickedEmoji: string) => { : (pickedEmoji: string) => {
selectDraftEmojiToBeReplaced( selectDraftEmojiToBeReplaced(
emojis.findIndex(emoji => emoji === pickedEmoji) draftPreferredReactions.findIndex(emoji => emoji === pickedEmoji)
); );
}; };
@ -117,7 +113,12 @@ export function CustomizingPreferredReactionsModal({
); );
const canReset = const canReset =
!isSaving && !isSaving &&
!isEqual(DEFAULT_PREFERRED_REACTION_EMOJI, draftPreferredReactions); !isEqual(
DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(shortName =>
convertShortName(shortName, skinTone)
),
draftPreferredReactions
);
const canSave = !isSaving && hasChanged; const canSave = !isSaving && hasChanged;
return ( return (
@ -140,7 +141,6 @@ export function CustomizingPreferredReactionsModal({
selected={selected} selected={selected}
selectionStyle={ReactionPickerSelectionStyle.Menu} selectionStyle={ReactionPickerSelectionStyle.Menu}
renderEmojiPicker={shouldNotBeCalled} renderEmojiPicker={shouldNotBeCalled}
skinTone={skinTone}
/> />
{hadSaveError {hadSaveError
? i18n('CustomizingPreferredReactions__had-save-error') ? i18n('CustomizingPreferredReactions__had-save-error')
@ -155,8 +155,12 @@ export function CustomizingPreferredReactionsModal({
> >
<EmojiPicker <EmojiPicker
i18n={i18n} i18n={i18n}
onPickEmoji={({ shortName }) => { onPickEmoji={pickedEmoji => {
replaceSelectedDraftEmoji(shortName); const emoji = convertShortName(
pickedEmoji.shortName,
pickedEmoji.skinTone
);
replaceSelectedDraftEmoji(emoji);
}} }}
skinTone={skinTone} skinTone={skinTone}
onSetSkinTone={onSetSkinTone} onSetSkinTone={onSetSkinTone}

View file

@ -6,7 +6,6 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { select } from '@storybook/addon-knobs';
import { setup as setupI18n } from '../../../js/modules/i18n'; import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json'; import enMessages from '../../../_locales/en/messages.json';
import { import {
@ -18,14 +17,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
const preferredReactionEmoji = [ const preferredReactionEmoji = ['❤️', '👍', '👎', '😂', '😮', '😢'];
'heart',
'thumbsup',
'thumbsdown',
'joy',
'open_mouth',
'cry',
];
const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({ const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
onClose, onClose,
@ -56,7 +48,6 @@ storiesOf('Components/Conversation/ReactionPicker', module)
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
selectionStyle={ReactionPickerSelectionStyle.Picker} selectionStyle={ReactionPickerSelectionStyle.Picker}
skinTone={0}
/> />
); );
}) })
@ -74,30 +65,6 @@ storiesOf('Components/Conversation/ReactionPicker', module)
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
renderEmojiPicker={renderEmojiPicker} renderEmojiPicker={renderEmojiPicker}
selectionStyle={ReactionPickerSelectionStyle.Picker} 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> </div>
)); ));

View file

@ -10,6 +10,7 @@ import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus'; import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { canCustomizePreferredReactions } from '../../util/canCustomizePreferredReactions';
export enum ReactionPickerSelectionStyle { export enum ReactionPickerSelectionStyle {
Picker, Picker,
@ -35,7 +36,6 @@ export type OwnProps = {
openCustomizePreferredReactionsModal?: () => unknown; openCustomizePreferredReactionsModal?: () => unknown;
preferredReactionEmoji: Array<string>; preferredReactionEmoji: Array<string>;
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement; renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
skinTone: number;
}; };
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>; export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
@ -81,7 +81,6 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
renderEmojiPicker, renderEmojiPicker,
selected, selected,
selectionStyle, selectionStyle,
skinTone,
style, style,
}, },
ref ref
@ -116,7 +115,9 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
if (pickingOther) { if (pickingOther) {
return renderEmojiPicker({ return renderEmojiPicker({
onClickSettings: openCustomizePreferredReactionsModal, onClickSettings: canCustomizePreferredReactions()
? openCustomizePreferredReactionsModal
: undefined,
onClose, onClose,
onPickEmoji, onPickEmoji,
onSetSkinTone, onSetSkinTone,
@ -125,11 +126,8 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
}); });
} }
const emojis = preferredReactionEmoji.map(shortName => const otherSelected =
convertShortName(shortName, skinTone) selected && !preferredReactionEmoji.includes(selected);
);
const otherSelected = selected && !emojis.includes(selected);
let moreButton: React.ReactNode; let moreButton: React.ReactNode;
if (!hasMoreButton) { if (!hasMoreButton) {
@ -189,7 +187,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
selected ? 'module-ReactionPicker--something-selected' : undefined selected ? 'module-ReactionPicker--something-selected' : undefined
)} )}
> >
{emojis.map((emoji, index) => { {preferredReactionEmoji.map((emoji, index) => {
const maybeFocusRef = index === 0 ? focusRef : undefined; const maybeFocusRef = index === 0 ? focusRef : undefined;
return ( return (

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
export const DEFAULT_PREFERRED_REACTION_EMOJI = [ export const DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES = [
'heart', 'heart',
'thumbsup', 'thumbsup',
'thumbsdown', 'thumbsdown',

View file

@ -1,18 +1,27 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { DEFAULT_PREFERRED_REACTION_EMOJI } from './constants'; import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from './constants';
import * as emoji from '../components/emoji/lib'; 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 = const isStoredValueValid =
Array.isArray(storedValue) && Array.isArray(storedValue) &&
storedValue.length === PREFERRED_REACTION_EMOJI_COUNT && storedValue.length === PREFERRED_REACTION_EMOJI_COUNT &&
storedValue.every(emoji.isShortName) && storedValue.every(isValidReactionEmoji) &&
!hasDuplicates(storedValue); !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 { function hasDuplicates(arr: ReadonlyArray<unknown>): boolean {

View file

@ -8,8 +8,10 @@ import * as Errors from '../../types/errors';
import { replaceIndex } from '../../util/replaceIndex'; import { replaceIndex } from '../../util/replaceIndex';
import { useBoundActions } from '../../util/hooks'; import { useBoundActions } from '../../util/hooks';
import type { StateType as RootStateType } from '../reducer'; 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 { getPreferredReactionEmoji } from '../../reactions/getPreferredReactionEmoji';
import { getEmojiSkinTone } from '../selectors/items';
import { convertShortName } from '../../components/emoji/lib';
// State // State
@ -61,7 +63,10 @@ type ReplaceSelectedDraftEmojiActionType = {
payload: string; payload: string;
}; };
type ResetDraftEmojiActionType = { type: typeof RESET_DRAFT_EMOJI }; type ResetDraftEmojiActionType = {
type: typeof RESET_DRAFT_EMOJI;
payload: { skinTone: number };
};
type SavePreferredReactionsFulfilledActionType = { type SavePreferredReactionsFulfilledActionType = {
type: typeof SAVE_PREFERRED_REACTIONS_FULFILLED; type: typeof SAVE_PREFERRED_REACTIONS_FULFILLED;
@ -109,8 +114,10 @@ function openCustomizePreferredReactionsModal(): ThunkAction<
OpenCustomizePreferredReactionsModalActionType OpenCustomizePreferredReactionsModalActionType
> { > {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState();
const originalPreferredReactions = getPreferredReactionEmoji( const originalPreferredReactions = getPreferredReactionEmoji(
getState().items.preferredReactionEmoji getState().items.preferredReactionEmoji,
getEmojiSkinTone(state)
); );
dispatch({ dispatch({
type: OPEN_CUSTOMIZE_PREFERRED_REACTIONS_MODAL, type: OPEN_CUSTOMIZE_PREFERRED_REACTIONS_MODAL,
@ -128,8 +135,16 @@ function replaceSelectedDraftEmoji(
}; };
} }
function resetDraftEmoji(): ResetDraftEmojiActionType { function resetDraftEmoji(): ThunkAction<
return { type: RESET_DRAFT_EMOJI }; void,
RootStateType,
unknown,
ResetDraftEmojiActionType
> {
return (dispatch, getState) => {
const skinTone = getEmojiSkinTone(getState());
dispatch({ type: RESET_DRAFT_EMOJI, payload: { skinTone } });
};
} }
function savePreferredReactions(): ThunkAction< 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) { if (!state.customizePreferredReactionsModal) {
return state; return state;
} }
@ -261,10 +277,13 @@ export function reducer(
...state, ...state,
customizePreferredReactionsModal: { customizePreferredReactionsModal: {
...state.customizePreferredReactionsModal, ...state.customizePreferredReactionsModal,
draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI, draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.map(
shortName => convertShortName(shortName, skinTone)
),
selectedDraftEmojiIndex: undefined, selectedDraftEmojiIndex: undefined,
}, },
}; };
}
case SAVE_PREFERRED_REACTIONS_PENDING: case SAVE_PREFERRED_REACTIONS_PENDING:
if (!state.customizePreferredReactionsModal) { if (!state.customizePreferredReactionsModal) {
return state; return state;

View file

@ -65,6 +65,10 @@ export const getEmojiSkinTone = createSelector(
export const getPreferredReactionEmoji = createSelector( export const getPreferredReactionEmoji = createSelector(
getItems, getItems,
(state: Readonly<ItemsStateType>): Array<string> => getEmojiSkinTone,
getPreferredReactionEmojiFromStoredValue(state.preferredReactionEmoji) (state: Readonly<ItemsStateType>, skinTone: number): Array<string> =>
getPreferredReactionEmojiFromStoredValue(
state.preferredReactionEmoji,
skinTone
)
); );

View file

@ -8,10 +8,7 @@ import { useActions as usePreferredReactionsActions } from '../ducks/preferredRe
import { useActions as useItemsActions } from '../ducks/items'; import { useActions as useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { import { getPreferredReactionEmoji } from '../selectors/items';
getEmojiSkinTone,
getPreferredReactionEmoji,
} from '../selectors/items';
import { LocalizerType } from '../../types/Util'; import { LocalizerType } from '../../types/Util';
import { import {
@ -45,10 +42,6 @@ export const SmartReactionPicker = React.forwardRef<
getPreferredReactionEmoji getPreferredReactionEmoji
); );
const skinTone = useSelector<StateType, number>(state =>
getEmojiSkinTone(state)
);
return ( return (
<ReactionPicker <ReactionPicker
i18n={i18n} i18n={i18n}
@ -59,7 +52,6 @@ export const SmartReactionPicker = React.forwardRef<
preferredReactionEmoji={preferredReactionEmoji} preferredReactionEmoji={preferredReactionEmoji}
ref={ref} ref={ref}
selectionStyle={ReactionPickerSelectionStyle.Picker} selectionStyle={ReactionPickerSelectionStyle.Picker}
skinTone={skinTone}
{...props} {...props}
/> />
); );

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai'; 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'; import { getPreferredReactionEmoji } from '../../reactions/getPreferredReactionEmoji';
@ -12,35 +12,34 @@ describe('getPreferredReactionEmoji', () => {
// Invalid types // Invalid types
undefined, undefined,
null, null,
DEFAULT_PREFERRED_REACTION_EMOJI.join(','), DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.join(','),
// Invalid lengths // Invalid lengths
[], [],
DEFAULT_PREFERRED_REACTION_EMOJI.slice(0, 3), DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES.slice(0, 3),
[...DEFAULT_PREFERRED_REACTION_EMOJI, 'sparkles'], [...DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES, '✨'],
// Non-strings in the array // Non-strings in the array
['heart', 'thumbsdown', undefined, 'joy', 'open_mouth', 'cry'], ['❤️', '👍', undefined, '😂', '😮', '😢'],
['heart', 'thumbsdown', 99, 'joy', 'open_mouth', 'cry'], ['❤️', '👍', 99, '😂', '😮', '😢'],
// Invalid emoji // Invalid emoji
['heart', 'thumbsdown', 'gorbage!!', 'joy', 'open_mouth', 'cry'], ['❤️', '👍', 'x', '😂', '😮', '😢'],
['❤️', '👍', 'garbage!!', '😂', '😮', '😢'],
['❤️', '👍', '✨✨', '😂', '😮', '😢'],
// Has duplicates // Has duplicates
['heart', 'thumbsdown', 'joy', 'joy', 'open_mouth', 'cry'], ['❤️', '👍', '👍', '😂', '😮', '😢'],
].forEach(input => { ].forEach(input => {
assert.deepStrictEqual( assert.deepStrictEqual(getPreferredReactionEmoji(input, 2), [
getPreferredReactionEmoji(input), '❤️',
DEFAULT_PREFERRED_REACTION_EMOJI '👍🏼',
); '👎🏼',
'😂',
'😮',
'😢',
]);
}); });
}); });
it('returns a custom set if passed a valid value', () => { it('returns a custom set if passed a valid value', () => {
const input = [ const input = ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'];
'sparkles', assert.deepStrictEqual(getPreferredReactionEmoji(input, 3), input);
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'parking',
];
assert.deepStrictEqual(getPreferredReactionEmoji(input), input);
}); });
}); });

View file

@ -26,6 +26,8 @@ describe('isValidReactionEmoji', () => {
it('returns true for strings that are exactly 1 emoji', () => { it('returns true for strings that are exactly 1 emoji', () => {
assert.isTrue(isValidReactionEmoji('🇺🇸')); assert.isTrue(isValidReactionEmoji('🇺🇸'));
assert.isTrue(isValidReactionEmoji('👍'));
assert.isTrue(isValidReactionEmoji('👍🏾'));
assert.isTrue(isValidReactionEmoji('👩‍❤️‍👩')); assert.isTrue(isValidReactionEmoji('👩‍❤️‍👩'));
}); });
}); });

View file

@ -3,9 +3,8 @@
import { assert } from 'chai'; import { assert } from 'chai';
import * as sinon from 'sinon'; 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 { noopAction } from '../../../state/ducks/noop';
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../../reactions/constants';
import { import {
PreferredReactionsStateType, PreferredReactionsStateType,
@ -15,9 +14,12 @@ import {
} from '../../../state/ducks/preferredReactions'; } from '../../../state/ducks/preferredReactions';
describe('preferred reactions duck', () => { 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(), ...getEmptyRootState(),
preferredReactions, preferredReactions,
}); });
@ -25,22 +27,8 @@ describe('preferred reactions duck', () => {
const stateWithOpenCustomizationModal = { const stateWithOpenCustomizationModal = {
...getInitialState(), ...getInitialState(),
customizePreferredReactionsModal: { customizePreferredReactionsModal: {
draftPreferredReactions: [ draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
'sparkles', originalPreferredReactions: ['💙', '👍', '👎', '😂', '😮', '😢'],
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'parking',
],
originalPreferredReactions: [
'blue_heart',
'thumbsup',
'thumbsdown',
'joy',
'open_mouth',
'cry',
],
selectedDraftEmojiIndex: undefined, selectedDraftEmojiIndex: undefined,
isSaving: false as const, isSaving: false as const,
hadSaveError: false, hadSaveError: false,
@ -120,15 +108,26 @@ describe('preferred reactions duck', () => {
const { openCustomizePreferredReactionsModal } = actions; const { openCustomizePreferredReactionsModal } = actions;
it('opens the customization modal with defaults if no value was stored', () => { 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(); const dispatch = sinon.spy();
openCustomizePreferredReactionsModal()(dispatch, getEmptyRootState, null); openCustomizePreferredReactionsModal()(dispatch, () => rootState, null);
const [action] = dispatch.getCall(0).args; const [action] = dispatch.getCall(0).args;
const result = reducer(getEmptyRootState().preferredReactions, action); const result = reducer(rootState.preferredReactions, action);
const expectedEmoji = ['❤️', '👍🏿', '👎🏿', '😂', '😮', '😢'];
assert.deepEqual(result.customizePreferredReactionsModal, { assert.deepEqual(result.customizePreferredReactionsModal, {
draftPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI, draftPreferredReactions: expectedEmoji,
originalPreferredReactions: DEFAULT_PREFERRED_REACTION_EMOJI, originalPreferredReactions: expectedEmoji,
selectedDraftEmojiIndex: undefined, selectedDraftEmojiIndex: undefined,
isSaving: false, isSaving: false,
hadSaveError: false, hadSaveError: false,
@ -136,14 +135,7 @@ describe('preferred reactions duck', () => {
}); });
it('opens the customization modal with stored values', () => { it('opens the customization modal with stored values', () => {
const storedPreferredReactionEmoji = [ const storedPreferredReactionEmoji = ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'];
'sparkles',
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'parking',
];
const emptyRootState = getEmptyRootState(); const emptyRootState = getEmptyRootState();
const state = { const state = {
@ -175,21 +167,21 @@ describe('preferred reactions duck', () => {
it('is a no-op if the customization modal is not open', () => { it('is a no-op if the customization modal is not open', () => {
const state = getInitialState(); const state = getInitialState();
const action = replaceSelectedDraftEmoji('cat'); const action = replaceSelectedDraftEmoji('🦈');
const result = reducer(state, action); const result = reducer(state, action);
assert.strictEqual(result, state); assert.strictEqual(result, state);
}); });
it('is a no-op if no emoji is selected', () => { it('is a no-op if no emoji is selected', () => {
const action = replaceSelectedDraftEmoji('cat'); const action = replaceSelectedDraftEmoji('💅');
const result = reducer(stateWithOpenCustomizationModal, action); const result = reducer(stateWithOpenCustomizationModal, action);
assert.strictEqual(result, stateWithOpenCustomizationModal); assert.strictEqual(result, stateWithOpenCustomizationModal);
}); });
it('is a no-op if the new emoji is already in the list', () => { it('is a no-op if the new emoji is already in the list', () => {
const action = replaceSelectedDraftEmoji('shark'); const action = replaceSelectedDraftEmoji('');
const result = reducer( const result = reducer(
stateWithOpenCustomizationModalAndSelectedEmoji, stateWithOpenCustomizationModalAndSelectedEmoji,
action action
@ -202,7 +194,7 @@ describe('preferred reactions duck', () => {
}); });
it('replaces the selected draft emoji and deselects', () => { it('replaces the selected draft emoji and deselects', () => {
const action = replaceSelectedDraftEmoji('cat'); const action = replaceSelectedDraftEmoji('🐱');
const result = reducer( const result = reducer(
stateWithOpenCustomizationModalAndSelectedEmoji, stateWithOpenCustomizationModalAndSelectedEmoji,
action action
@ -210,7 +202,7 @@ describe('preferred reactions duck', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
result.customizePreferredReactionsModal?.draftPreferredReactions, result.customizePreferredReactionsModal?.draftPreferredReactions,
['sparkles', 'cat', 'sparkler', 'shark', 'sparkling_heart', 'parking'] ['✨', '🐱', '🎇', '🦈', '💖', '🅿️']
); );
assert.isUndefined( assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex result.customizePreferredReactionsModal?.selectedDraftEmojiIndex
@ -221,30 +213,39 @@ describe('preferred reactions duck', () => {
describe('resetDraftEmoji', () => { describe('resetDraftEmoji', () => {
const { resetDraftEmoji } = actions; 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', () => { it('is a no-op if the customization modal is not open', () => {
const state = getInitialState(); const rootState = getEmptyRootState();
const action = resetDraftEmoji(); const state = rootState.preferredReactions;
const action = getAction(rootState);
const result = reducer(state, action); const result = reducer(state, action);
assert.strictEqual(result, state); assert.strictEqual(result, state);
}); });
it('resets the draft emoji to the defaults', () => { it('resets the draft emoji to the defaults', () => {
const action = resetDraftEmoji(); const rootState = getRootState(stateWithOpenCustomizationModal);
const result = reducer(stateWithOpenCustomizationModal, action); const action = getAction(rootState);
const result = reducer(rootState.preferredReactions, action);
assert.deepEqual( assert.deepEqual(
result.customizePreferredReactionsModal?.draftPreferredReactions, result.customizePreferredReactionsModal?.draftPreferredReactions,
DEFAULT_PREFERRED_REACTION_EMOJI ['❤️', '👍', '👎', '😂', '😮', '😢']
); );
}); });
it('deselects any selected emoji', () => { it('deselects any selected emoji', () => {
const action = resetDraftEmoji(); const rootState = getRootState(
const result = reducer( stateWithOpenCustomizationModalAndSelectedEmoji
stateWithOpenCustomizationModalAndSelectedEmoji,
action
); );
const action = getAction(rootState);
const result = reducer(rootState.preferredReactions, action);
assert.isUndefined( assert.isUndefined(
result.customizePreferredReactionsModal?.selectedDraftEmojiIndex result.customizePreferredReactionsModal?.selectedDraftEmojiIndex

View file

@ -9,7 +9,6 @@ import {
} from '../../../state/selectors/items'; } from '../../../state/selectors/items';
import type { StateType } from '../../../state/reducer'; import type { StateType } from '../../../state/reducer';
import type { ItemsStateType } from '../../../state/ducks/items'; import type { ItemsStateType } from '../../../state/ducks/items';
import { DEFAULT_PREFERRED_REACTION_EMOJI } from '../../../reactions/constants';
describe('both/state/selectors/items', () => { describe('both/state/selectors/items', () => {
// Note: we would like to use the full reducer here, to get a real empty state object // 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', () => { describe('#getPreferredReactionEmoji', () => {
// See also: the tests for the `getPreferredReactionEmoji` helper. // See also: the tests for the `getPreferredReactionEmoji` helper.
const expectedDefault = ['❤️', '👍🏿', '👎🏿', '😂', '😮', '😢'];
it('returns the default set if no value is stored', () => { it('returns the default set if no value is stored', () => {
const state = getRootState({}); const state = getRootState({ skinTone: 5 });
const actual = getPreferredReactionEmoji(state); 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', () => { 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); const actual = getPreferredReactionEmoji(state);
assert.deepStrictEqual(actual, DEFAULT_PREFERRED_REACTION_EMOJI); assert.deepStrictEqual(actual, expectedDefault);
}); });
it('returns a custom set of emoji', () => { it('returns a custom set of emoji', () => {
const preferredReactionEmoji = [ const preferredReactionEmoji = ['✨', '❇️', '🤙🏻', '🦈', '💖', '🅿️'];
'sparkles', const state = getRootState({ skinTone: 5, preferredReactionEmoji });
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'parking',
];
const state = getRootState({ preferredReactionEmoji });
const actual = getPreferredReactionEmoji(state); const actual = getPreferredReactionEmoji(state);
assert.deepStrictEqual(actual, preferredReactionEmoji); assert.deepStrictEqual(actual, preferredReactionEmoji);

View file

@ -28,22 +28,8 @@ describe('both/state/selectors/preferredReactions', () => {
getIsCustomizingPreferredReactions( getIsCustomizingPreferredReactions(
getRootState({ getRootState({
customizePreferredReactionsModal: { customizePreferredReactionsModal: {
draftPreferredReactions: [ draftPreferredReactions: ['✨', '❇️', '🎇', '🦈', '💖', '🅿️'],
'sparkles', originalPreferredReactions: ['💙', '👍', '👎', '😂', '😮', '😢'],
'sparkle',
'sparkler',
'shark',
'sparkling_heart',
'parking',
],
originalPreferredReactions: [
'blue_heart',
'thumbsup',
'thumbsdown',
'joy',
'open_mouth',
'cry',
],
selectedDraftEmojiIndex: undefined, selectedDraftEmojiIndex: undefined,
isSaving: false as const, isSaving: false as const,
hadSaveError: false, hadSaveError: false,

View 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')
);
}