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 =
|
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'
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
));
|
));
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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('👩❤️👩'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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…
Add table
Add a link
Reference in a new issue