From e2392433e006e4bcba5052799f2d499971492301 Mon Sep 17 00:00:00 2001 From: Evan Hahn <69474926+EvanHahn-Signal@users.noreply.github.com> Date: Thu, 9 Sep 2021 18:47:30 -0500 Subject: [PATCH] Preferred reactions: store raw emoji, gate on feature flag --- ts/RemoteConfig.ts | 1 + ...omizingPreferredReactionsModal.stories.tsx | 18 +--- .../CustomizingPreferredReactionsModal.tsx | 26 +++--- .../conversation/ReactionPicker.stories.tsx | 35 +------ ts/components/conversation/ReactionPicker.tsx | 16 ++-- ts/reactions/constants.ts | 2 +- ts/reactions/getPreferredReactionEmoji.ts | 21 +++-- ts/state/ducks/preferredReactions.ts | 33 +++++-- ts/state/selectors/items.ts | 8 +- ts/state/smart/ReactionPicker.tsx | 10 +- .../getPreferredReactionEmoji_test.ts | 41 ++++---- .../reactions/isValidReactionEmoji_test.ts | 2 + .../state/ducks/preferredReactions_test.ts | 93 ++++++++++--------- ts/test-both/state/selectors/items_test.ts | 25 +++-- .../selectors/preferredReactions_test.ts | 18 +--- ts/util/canCustomizePreferredReactions.ts | 11 +++ 16 files changed, 168 insertions(+), 192 deletions(-) create mode 100644 ts/util/canCustomizePreferredReactions.ts diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index d6fcd1abcad7..abc5daf6dcb8 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -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' diff --git a/ts/components/CustomizingPreferredReactionsModal.stories.tsx b/ts/components/CustomizingPreferredReactionsModal.stories.tsx index 21aba853136b..5fa3aa11bef3 100644 --- a/ts/components/CustomizingPreferredReactionsModal.stories.tsx +++ b/ts/components/CustomizingPreferredReactionsModal.stories.tsx @@ -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'), diff --git a/ts/components/CustomizingPreferredReactionsModal.tsx b/ts/components/CustomizingPreferredReactionsModal.tsx index 36e3b3759d06..0d04bd018e3c 100644 --- a/ts/components/CustomizingPreferredReactionsModal.tsx +++ b/ts/components/CustomizingPreferredReactionsModal.tsx @@ -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({ > { - replaceSelectedDraftEmoji(shortName); + onPickEmoji={pickedEmoji => { + const emoji = convertShortName( + pickedEmoji.shortName, + pickedEmoji.skinTone + ); + replaceSelectedDraftEmoji(emoji); }} skinTone={skinTone} onSetSkinTone={onSetSkinTone} diff --git a/ts/components/conversation/ReactionPicker.stories.tsx b/ts/components/conversation/ReactionPicker.stories.tsx index 659e3906f566..83792d33b024 100644 --- a/ts/components/conversation/ReactionPicker.stories.tsx +++ b/ts/components/conversation/ReactionPicker.stories.tsx @@ -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} - /> - - )); - }) - .add('Skin Tones', () => { - return ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => ( -
-
)); diff --git a/ts/components/conversation/ReactionPicker.tsx b/ts/components/conversation/ReactionPicker.tsx index f5b4a40ada13..7bd62baee16e 100644 --- a/ts/components/conversation/ReactionPicker.tsx +++ b/ts/components/conversation/ReactionPicker.tsx @@ -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; renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement; - skinTone: number; }; export type Props = OwnProps & Pick, 'style'>; @@ -81,7 +81,6 @@ export const ReactionPicker = React.forwardRef( renderEmojiPicker, selected, selectionStyle, - skinTone, style, }, ref @@ -116,7 +115,9 @@ export const ReactionPicker = React.forwardRef( if (pickingOther) { return renderEmojiPicker({ - onClickSettings: openCustomizePreferredReactionsModal, + onClickSettings: canCustomizePreferredReactions() + ? openCustomizePreferredReactionsModal + : undefined, onClose, onPickEmoji, onSetSkinTone, @@ -125,11 +126,8 @@ export const ReactionPicker = React.forwardRef( }); } - 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( selected ? 'module-ReactionPicker--something-selected' : undefined )} > - {emojis.map((emoji, index) => { + {preferredReactionEmoji.map((emoji, index) => { const maybeFocusRef = index === 0 ? focusRef : undefined; return ( diff --git a/ts/reactions/constants.ts b/ts/reactions/constants.ts index 7e34ea72f5e3..b33c9f3c0f6f 100644 --- a/ts/reactions/constants.ts +++ b/ts/reactions/constants.ts @@ -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', diff --git a/ts/reactions/getPreferredReactionEmoji.ts b/ts/reactions/getPreferredReactionEmoji.ts index 55d9a95064e1..704c40d566ad 100644 --- a/ts/reactions/getPreferredReactionEmoji.ts +++ b/ts/reactions/getPreferredReactionEmoji.ts @@ -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 { +export function getPreferredReactionEmoji( + storedValue: unknown, + skinTone: number +): Array { 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): boolean { diff --git a/ts/state/ducks/preferredReactions.ts b/ts/state/ducks/preferredReactions.ts index c0fa74ef551d..87c593ddc566 100644 --- a/ts/state/ducks/preferredReactions.ts +++ b/ts/state/ducks/preferredReactions.ts @@ -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; diff --git a/ts/state/selectors/items.ts b/ts/state/selectors/items.ts index d4326ae2bc07..bec0c6a7bb74 100644 --- a/ts/state/selectors/items.ts +++ b/ts/state/selectors/items.ts @@ -65,6 +65,10 @@ export const getEmojiSkinTone = createSelector( export const getPreferredReactionEmoji = createSelector( getItems, - (state: Readonly): Array => - getPreferredReactionEmojiFromStoredValue(state.preferredReactionEmoji) + getEmojiSkinTone, + (state: Readonly, skinTone: number): Array => + getPreferredReactionEmojiFromStoredValue( + state.preferredReactionEmoji, + skinTone + ) ); diff --git a/ts/state/smart/ReactionPicker.tsx b/ts/state/smart/ReactionPicker.tsx index af2ff74549e0..ca67b751cb82 100644 --- a/ts/state/smart/ReactionPicker.tsx +++ b/ts/state/smart/ReactionPicker.tsx @@ -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(state => - getEmojiSkinTone(state) - ); - return ( ); diff --git a/ts/test-both/reactions/getPreferredReactionEmoji_test.ts b/ts/test-both/reactions/getPreferredReactionEmoji_test.ts index 1f44b2efe793..ae135ad7dc34 100644 --- a/ts/test-both/reactions/getPreferredReactionEmoji_test.ts +++ b/ts/test-both/reactions/getPreferredReactionEmoji_test.ts @@ -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); }); }); diff --git a/ts/test-both/reactions/isValidReactionEmoji_test.ts b/ts/test-both/reactions/isValidReactionEmoji_test.ts index cedd267cd691..c9d1070ac97d 100644 --- a/ts/test-both/reactions/isValidReactionEmoji_test.ts +++ b/ts/test-both/reactions/isValidReactionEmoji_test.ts @@ -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('👩‍❤️‍👩')); }); }); diff --git a/ts/test-both/state/ducks/preferredReactions_test.ts b/ts/test-both/state/ducks/preferredReactions_test.ts index 14efa8040cbe..a0723c54d7db 100644 --- a/ts/test-both/state/ducks/preferredReactions_test.ts +++ b/ts/test-both/state/ducks/preferredReactions_test.ts @@ -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) { + 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 diff --git a/ts/test-both/state/selectors/items_test.ts b/ts/test-both/state/selectors/items_test.ts index 21d014a977ba..75f22646f94f 100644 --- a/ts/test-both/state/selectors/items_test.ts +++ b/ts/test-both/state/selectors/items_test.ts @@ -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); diff --git a/ts/test-both/state/selectors/preferredReactions_test.ts b/ts/test-both/state/selectors/preferredReactions_test.ts index 62a99b5d4873..0564e792c1ba 100644 --- a/ts/test-both/state/selectors/preferredReactions_test.ts +++ b/ts/test-both/state/selectors/preferredReactions_test.ts @@ -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, diff --git a/ts/util/canCustomizePreferredReactions.ts b/ts/util/canCustomizePreferredReactions.ts new file mode 100644 index 000000000000..53d8b10d3607 --- /dev/null +++ b/ts/util/canCustomizePreferredReactions.ts @@ -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') + ); +}