Pre-alpha: React with any emoji, behind flag
This commit is contained in:
parent
d13c3d3350
commit
0865a5481c
31 changed files with 572 additions and 234 deletions
|
@ -2296,6 +2296,10 @@
|
||||||
"message": "Failed to send reaction. Please try again.",
|
"message": "Failed to send reaction. Please try again.",
|
||||||
"description": "Shown when a reaction fails to send"
|
"description": "Shown when a reaction fails to send"
|
||||||
},
|
},
|
||||||
|
"ReactionsViewer--more": {
|
||||||
|
"message": "More",
|
||||||
|
"description": "Use in the reaction picker as the alt text for the 'more' button"
|
||||||
|
},
|
||||||
"ReactionsViewer--all": {
|
"ReactionsViewer--all": {
|
||||||
"message": "All",
|
"message": "All",
|
||||||
"description": "Shown in reaction viewer as the title for the 'all' category"
|
"description": "Shown in reaction viewer as the title for the 'all' category"
|
||||||
|
|
6
images/any-emoji-32-hover.svg
Normal file
6
images/any-emoji-32-hover.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#4A4A4A"/>
|
||||||
|
<circle cx="8" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
<circle cx="16" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
<circle cx="24" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
6
images/any-emoji-32.svg
Normal file
6
images/any-emoji-32.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#2E2E2E"/>
|
||||||
|
<circle cx="8" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
<circle cx="16" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
<circle cx="24" cy="16" r="2" fill="#E9E9E9"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
|
@ -15,6 +15,8 @@ try {
|
||||||
|
|
||||||
// Derive profile key versions, then use those to fetch versioned profiles from server
|
// Derive profile key versions, then use those to fetch versioned profiles from server
|
||||||
window.VERSIONED_PROFILE_FETCH = false;
|
window.VERSIONED_PROFILE_FETCH = false;
|
||||||
|
// Enable full emoji picker for reactions
|
||||||
|
window.REACT_ANY_EMOJI = false;
|
||||||
|
|
||||||
window.PROTO_ROOT = 'protos';
|
window.PROTO_ROOT = 'protos';
|
||||||
const config = require('url').parse(window.location.toString(), true).query;
|
const config = require('url').parse(window.location.toString(), true).query;
|
||||||
|
|
|
@ -5163,6 +5163,23 @@ button.module-image__border-overlay:focus {
|
||||||
// This color is the same in both light and dark themes
|
// This color is the same in both light and dark themes
|
||||||
background: rgba($color-white, 0.3);
|
background: rgba($color-white, 0.3);
|
||||||
}
|
}
|
||||||
|
&--more {
|
||||||
|
background: url('../images/any-emoji-32.svg') no-repeat center;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
background: url('../images/any-emoji-32-hover.svg') no-repeat center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 400ms $ease-out-expo;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@include keyboard-mode {
|
@include keyboard-mode {
|
||||||
&:focus:before {
|
&:focus:before {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { Avatar, Props as AvatarProps } from './Avatar';
|
import { Avatar, Props as AvatarProps } from './Avatar';
|
||||||
import { useRestoreFocus } from './hooks';
|
import { useRestoreFocus } from '../util/hooks';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRestoreFocus } from './hooks';
|
import { useRestoreFocus } from '../util/hooks';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
|
|
@ -8,7 +8,14 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
import { Message, PropsActions, PropsData, PropsHousekeeping } from './Message';
|
import {
|
||||||
|
Message,
|
||||||
|
Props as AllProps,
|
||||||
|
PropsActions,
|
||||||
|
PropsData,
|
||||||
|
PropsHousekeeping,
|
||||||
|
} from './Message';
|
||||||
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
|
|
||||||
const book = storiesOf('Components/Conversation/Message', module);
|
const book = storiesOf('Components/Conversation/Message', module);
|
||||||
|
|
||||||
|
@ -1267,6 +1274,21 @@ const stories: Array<MessageStory> = [
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const renderEmojiPicker: AllProps['renderEmojiPicker'] = ({
|
||||||
|
onClose,
|
||||||
|
onPickEmoji,
|
||||||
|
ref,
|
||||||
|
}) => (
|
||||||
|
<EmojiPicker
|
||||||
|
i18n={setupI18n('en', enMessages)}
|
||||||
|
skinTone={0}
|
||||||
|
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||||
|
ref={ref}
|
||||||
|
onClose={onClose}
|
||||||
|
onPickEmoji={onPickEmoji}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
stories.forEach(([chapterTitle, propsArr]) =>
|
stories.forEach(([chapterTitle, propsArr]) =>
|
||||||
book.add(chapterTitle, () =>
|
book.add(chapterTitle, () =>
|
||||||
propsArr.map(
|
propsArr.map(
|
||||||
|
@ -1294,6 +1316,7 @@ stories.forEach(([chapterTitle, propsArr]) =>
|
||||||
{...dataProps}
|
{...dataProps}
|
||||||
{...makeActionProps()}
|
{...makeActionProps()}
|
||||||
{...makeHouseKeepingProps()}
|
{...makeHouseKeepingProps()}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
OwnProps as ReactionViewerProps,
|
OwnProps as ReactionViewerProps,
|
||||||
ReactionViewer,
|
ReactionViewer,
|
||||||
} from './ReactionViewer';
|
} from './ReactionViewer';
|
||||||
import { ReactionPicker } from './ReactionPicker';
|
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
|
||||||
import { Emoji } from '../emoji/Emoji';
|
import { Emoji } from '../emoji/Emoji';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -155,7 +155,10 @@ export type PropsActions = {
|
||||||
showExpiredOutgoingTapToViewToast: () => unknown;
|
showExpiredOutgoingTapToViewToast: () => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = PropsData & PropsHousekeeping & PropsActions;
|
export type Props = PropsData &
|
||||||
|
PropsHousekeeping &
|
||||||
|
PropsActions &
|
||||||
|
Pick<ReactionPickerProps, 'renderEmojiPicker'>;
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
expiring: boolean;
|
expiring: boolean;
|
||||||
|
@ -1001,9 +1004,11 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
canReply,
|
canReply,
|
||||||
direction,
|
direction,
|
||||||
disableMenu,
|
disableMenu,
|
||||||
|
i18n,
|
||||||
id,
|
id,
|
||||||
isSticker,
|
isSticker,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
|
renderEmojiPicker,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -1121,6 +1126,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<Popper placement="top">
|
<Popper placement="top">
|
||||||
{({ ref, style }) => (
|
{({ ref, style }) => (
|
||||||
<ReactionPicker
|
<ReactionPicker
|
||||||
|
i18n={i18n}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
style={style}
|
style={style}
|
||||||
selected={this.props.selectedReaction}
|
selected={this.props.selectedReaction}
|
||||||
|
@ -1132,6 +1138,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
remove: emoji === this.props.selectedReaction,
|
remove: emoji === this.props.selectedReaction,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popper>,
|
</Popper>,
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
### Reaction Picker
|
|
||||||
|
|
||||||
#### Base
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
|
|
||||||
<ReactionPicker onPick={e => console.log(`Picked reaction: ${e}`)} />
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Selected
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme} ios={util.ios} mode={util.mode}>
|
|
||||||
{['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
|
||||||
<div key={e} style={{ height: '100px' }}>
|
|
||||||
<ReactionPicker
|
|
||||||
selected={e}
|
|
||||||
onPick={e => console.log(`Picked reaction: ${e}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
51
ts/components/conversation/ReactionPicker.stories.tsx
Normal file
51
ts/components/conversation/ReactionPicker.stories.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
|
||||||
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const renderEmojiPicker: ReactionPickerProps['renderEmojiPicker'] = ({
|
||||||
|
onClose,
|
||||||
|
onPickEmoji,
|
||||||
|
ref,
|
||||||
|
}) => (
|
||||||
|
<EmojiPicker
|
||||||
|
i18n={i18n}
|
||||||
|
skinTone={0}
|
||||||
|
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||||
|
ref={ref}
|
||||||
|
onClose={onClose}
|
||||||
|
onPickEmoji={onPickEmoji}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/ReactionPicker', module)
|
||||||
|
.add('Base', () => {
|
||||||
|
return (
|
||||||
|
<ReactionPicker
|
||||||
|
i18n={i18n}
|
||||||
|
onPick={action('onPick')}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Selected Reaction', () => {
|
||||||
|
return ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'].map(e => (
|
||||||
|
<div key={e} style={{ height: '100px' }}>
|
||||||
|
<ReactionPicker
|
||||||
|
i18n={i18n}
|
||||||
|
selected={e}
|
||||||
|
onPick={action('onPick')}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
});
|
|
@ -1,20 +1,34 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Emoji } from '../emoji/Emoji';
|
import { Emoji } from '../emoji/Emoji';
|
||||||
import { useRestoreFocus } from '../hooks';
|
import { convertShortName } from '../emoji/lib';
|
||||||
|
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
||||||
|
import { useRestoreFocus } from '../../util/hooks';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export type RenderEmojiPickerProps = Pick<Props, 'onClose' | 'style'> &
|
||||||
|
Pick<EmojiPickerProps, 'onPickEmoji'> & {
|
||||||
|
ref: React.Ref<HTMLDivElement>;
|
||||||
|
};
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
|
i18n: LocalizerType;
|
||||||
selected?: string;
|
selected?: string;
|
||||||
onClose?: () => unknown;
|
onClose?: () => unknown;
|
||||||
onPick: (emoji: string) => unknown;
|
onPick: (emoji: string) => unknown;
|
||||||
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||||
|
|
||||||
const emojis = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
|
const emojis = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
|
||||||
|
|
||||||
|
const getEmojis = () =>
|
||||||
|
emojis.slice(0, window.REACT_ANY_EMOJI ? emojis.length - 1 : emojis.length);
|
||||||
|
|
||||||
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
({ selected, onClose, onPick, ...rest }, ref) => {
|
({ i18n, selected, onClose, onPick, renderEmojiPicker, style }, ref) => {
|
||||||
|
const [pickingOther, setPickingOther] = React.useState(false);
|
||||||
const focusRef = React.useRef<HTMLButtonElement>(null);
|
const focusRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
// Handle escape key
|
// Handle escape key
|
||||||
|
@ -32,12 +46,24 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
// Handle EmojiPicker::onPickEmoji
|
||||||
|
const onPickEmoji: EmojiPickerProps['onPickEmoji'] = React.useCallback(
|
||||||
|
({ shortName, skinTone }) => {
|
||||||
|
onPick(convertShortName(shortName, skinTone));
|
||||||
|
},
|
||||||
|
[onPick]
|
||||||
|
);
|
||||||
|
|
||||||
// Focus first button and restore focus on unmount
|
// Focus first button and restore focus on unmount
|
||||||
useRestoreFocus(focusRef);
|
useRestoreFocus(focusRef);
|
||||||
|
|
||||||
return (
|
const otherSelected = selected && !getEmojis().includes(selected);
|
||||||
<div {...rest} ref={ref} className="module-reaction-picker">
|
|
||||||
{emojis.map((emoji, index) => {
|
return pickingOther ? (
|
||||||
|
renderEmojiPicker({ onPickEmoji, onClose, style, ref })
|
||||||
|
) : (
|
||||||
|
<div ref={ref} style={style} className="module-reaction-picker">
|
||||||
|
{getEmojis().map((emoji, index) => {
|
||||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -55,6 +81,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onPick(emoji);
|
onPick(emoji);
|
||||||
}}
|
}}
|
||||||
|
title={emoji}
|
||||||
>
|
>
|
||||||
<div className="module-reaction-picker__emoji-btn__emoji">
|
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||||
<Emoji size={48} emoji={emoji} />
|
<Emoji size={48} emoji={emoji} />
|
||||||
|
@ -62,6 +89,31 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{window.REACT_ANY_EMOJI ? (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'module-reaction-picker__emoji-btn',
|
||||||
|
otherSelected
|
||||||
|
? 'module-reaction-picker__emoji-btn--selected'
|
||||||
|
: 'module-reaction-picker__emoji-btn--more'
|
||||||
|
)}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (otherSelected && selected) {
|
||||||
|
onPick(selected);
|
||||||
|
} else {
|
||||||
|
setPickingOther(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={i18n('ReactionsViewer--more')}
|
||||||
|
>
|
||||||
|
{otherSelected ? (
|
||||||
|
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||||
|
<Emoji size={48} emoji={selected} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Avatar, Props as AvatarProps } from '../Avatar';
|
import { Avatar, Props as AvatarProps } from '../Avatar';
|
||||||
import { Emoji } from '../emoji/Emoji';
|
import { Emoji } from '../emoji/Emoji';
|
||||||
import { useRestoreFocus } from '../hooks';
|
import { useRestoreFocus } from '../../util/hooks';
|
||||||
|
|
||||||
export type Reaction = {
|
export type Reaction = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
### A plain message
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const item = {
|
|
||||||
type: 'message',
|
|
||||||
data: {
|
|
||||||
id: 'id-1',
|
|
||||||
direction: 'incoming',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
authorPhoneNumber: '(202) 555-2001',
|
|
||||||
authorColor: 'green',
|
|
||||||
text: '🔥',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
<TimelineItem item={item} i18n={util.i18n} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### A notification
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const item = {
|
|
||||||
type: 'timerNotification',
|
|
||||||
data: {
|
|
||||||
type: 'fromOther',
|
|
||||||
phoneNumber: '(202) 555-0000',
|
|
||||||
timespan: '1 hour',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
<TimelineItem item={item} i18n={util.i18n} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unknown type
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const item = {
|
|
||||||
type: 'random',
|
|
||||||
data: {
|
|
||||||
somethin: 'somethin',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
<TimelineItem item={item} i18n={util.i18n} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Missing itme
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<TimelineItem item={null} i18n={util.i18n} />
|
|
||||||
```
|
|
104
ts/components/conversation/TimelineItem.stories.tsx
Normal file
104
ts/components/conversation/TimelineItem.stories.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
||||||
|
onClose,
|
||||||
|
onPickEmoji,
|
||||||
|
ref,
|
||||||
|
}) => (
|
||||||
|
<EmojiPicker
|
||||||
|
i18n={setupI18n('en', enMessages)}
|
||||||
|
skinTone={0}
|
||||||
|
onSetSkinTone={action('EmojiPicker::onSetSkinTone')}
|
||||||
|
ref={ref}
|
||||||
|
onClose={onClose}
|
||||||
|
onPickEmoji={onPickEmoji}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDefaultProps = () => ({
|
||||||
|
conversationId: 'conversation-id',
|
||||||
|
id: 'asdf',
|
||||||
|
isSelected: false,
|
||||||
|
selectMessage: action('selectMessage'),
|
||||||
|
reactToMessage: action('reactToMessage'),
|
||||||
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
|
replyToMessage: action('replyToMessage'),
|
||||||
|
retrySend: action('retrySend'),
|
||||||
|
deleteMessage: action('deleteMessage'),
|
||||||
|
showMessageDetail: action('showMessageDetail'),
|
||||||
|
openConversation: action('openConversation'),
|
||||||
|
showContactDetail: action('showContactDetail'),
|
||||||
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
|
downloadAttachment: action('downloadAttachment'),
|
||||||
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
'showExpiredIncomingTapToViewToast'
|
||||||
|
),
|
||||||
|
showExpiredOutgoingTapToViewToast: action(
|
||||||
|
'showExpiredIncomingTapToViewToast'
|
||||||
|
),
|
||||||
|
openLink: action('openLink'),
|
||||||
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
|
showIdentity: action('showIdentity'),
|
||||||
|
renderEmojiPicker,
|
||||||
|
});
|
||||||
|
|
||||||
|
storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
|
.add('Plain Message', () => {
|
||||||
|
const item = {
|
||||||
|
type: 'message',
|
||||||
|
data: {
|
||||||
|
id: 'id-1',
|
||||||
|
direction: 'incoming',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
authorPhoneNumber: '(202) 555-2001',
|
||||||
|
authorColor: 'green',
|
||||||
|
text: '🔥',
|
||||||
|
},
|
||||||
|
} as TimelineItemProps['item'];
|
||||||
|
|
||||||
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
|
})
|
||||||
|
.add('Notification', () => {
|
||||||
|
const item = {
|
||||||
|
type: 'timerNotification',
|
||||||
|
data: {
|
||||||
|
type: 'fromOther',
|
||||||
|
phoneNumber: '(202) 555-0000',
|
||||||
|
timespan: '1 hour',
|
||||||
|
},
|
||||||
|
} as TimelineItemProps['item'];
|
||||||
|
|
||||||
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
|
})
|
||||||
|
.add('Unknown Type', () => {
|
||||||
|
// @ts-ignore: intentional
|
||||||
|
const item = {
|
||||||
|
type: 'random',
|
||||||
|
data: {
|
||||||
|
somethin: 'somethin',
|
||||||
|
},
|
||||||
|
} as TimelineItemProps['item'];
|
||||||
|
|
||||||
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
|
})
|
||||||
|
.add('Missing Item', () => {
|
||||||
|
// @ts-ignore: intentional
|
||||||
|
const item = null as TimelineItemProps['item'];
|
||||||
|
|
||||||
|
return <TimelineItem {...getDefaultProps()} item={item} i18n={i18n} />;
|
||||||
|
});
|
|
@ -3,6 +3,7 @@ import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
|
Props as AllMessageProps,
|
||||||
PropsActions as MessageActionsType,
|
PropsActions as MessageActionsType,
|
||||||
PropsData as MessageProps,
|
PropsData as MessageProps,
|
||||||
} from './Message';
|
} from './Message';
|
||||||
|
@ -87,7 +88,9 @@ type PropsActionsType = MessageActionsType &
|
||||||
UnsupportedMessageActionsType &
|
UnsupportedMessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
type PropsType = PropsLocalType & PropsActionsType;
|
export type PropsType = PropsLocalType &
|
||||||
|
PropsActionsType &
|
||||||
|
Pick<AllMessageProps, 'renderEmojiPicker'>;
|
||||||
|
|
||||||
export class TimelineItem extends React.PureComponent<PropsType> {
|
export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
public render() {
|
public render() {
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
#### Default
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '500px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EmojiButton
|
|
||||||
i18n={util.i18n}
|
|
||||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
|
||||||
skinTone={0}
|
|
||||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
|
||||||
onClose={() => console.log('onClose')}
|
|
||||||
recentEmojis={[
|
|
||||||
'grinning',
|
|
||||||
'grin',
|
|
||||||
'joy',
|
|
||||||
'rolling_on_the_floor_laughing',
|
|
||||||
'smiley',
|
|
||||||
'smile',
|
|
||||||
'sweat_smile',
|
|
||||||
'laughing',
|
|
||||||
'wink',
|
|
||||||
'blush',
|
|
||||||
'yum',
|
|
||||||
'sunglasses',
|
|
||||||
'heart_eyes',
|
|
||||||
'kissing_heart',
|
|
||||||
'kissing',
|
|
||||||
'kissing_smiling_eyes',
|
|
||||||
'kissing_closed_eyes',
|
|
||||||
'relaxed',
|
|
||||||
'slightly_smiling_face',
|
|
||||||
'hugging_face',
|
|
||||||
'grinning_face_with_star_eyes',
|
|
||||||
'thinking_face',
|
|
||||||
'face_with_one_eyebrow_raised',
|
|
||||||
'neutral_face',
|
|
||||||
'expressionless',
|
|
||||||
'no_mouth',
|
|
||||||
'face_with_rolling_eyes',
|
|
||||||
'smirk',
|
|
||||||
'persevere',
|
|
||||||
'disappointed_relieved',
|
|
||||||
'open_mouth',
|
|
||||||
'zipper_mouth_face',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
66
ts/components/emoji/EmojiButton.stories.tsx
Normal file
66
ts/components/emoji/EmojiButton.stories.tsx
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { EmojiButton } from './EmojiButton';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf('Components/emojis/EmojiButton', module).add('Base', () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '500px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmojiButton
|
||||||
|
i18n={i18n}
|
||||||
|
onPickEmoji={action('onPickEmoji')}
|
||||||
|
skinTone={0}
|
||||||
|
onSetSkinTone={action('onSetSkinTone')}
|
||||||
|
recentEmojis={[
|
||||||
|
'grinning',
|
||||||
|
'grin',
|
||||||
|
'joy',
|
||||||
|
'rolling_on_the_floor_laughing',
|
||||||
|
'smiley',
|
||||||
|
'smile',
|
||||||
|
'sweat_smile',
|
||||||
|
'laughing',
|
||||||
|
'wink',
|
||||||
|
'blush',
|
||||||
|
'yum',
|
||||||
|
'sunglasses',
|
||||||
|
'heart_eyes',
|
||||||
|
'kissing_heart',
|
||||||
|
'kissing',
|
||||||
|
'kissing_smiling_eyes',
|
||||||
|
'kissing_closed_eyes',
|
||||||
|
'relaxed',
|
||||||
|
'slightly_smiling_face',
|
||||||
|
'hugging_face',
|
||||||
|
'grinning_face_with_star_eyes',
|
||||||
|
'thinking_face',
|
||||||
|
'face_with_one_eyebrow_raised',
|
||||||
|
'neutral_face',
|
||||||
|
'expressionless',
|
||||||
|
'no_mouth',
|
||||||
|
'face_with_rolling_eyes',
|
||||||
|
'smirk',
|
||||||
|
'persevere',
|
||||||
|
'disappointed_relieved',
|
||||||
|
'open_mouth',
|
||||||
|
'zipper_mouth_face',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
|
@ -1,60 +0,0 @@
|
||||||
#### Default
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme}>
|
|
||||||
<EmojiPicker
|
|
||||||
i18n={util.i18n}
|
|
||||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
|
||||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
|
||||||
onClose={() => console.log('onClose')}
|
|
||||||
recentEmojis={[
|
|
||||||
'grinning',
|
|
||||||
'grin',
|
|
||||||
'joy',
|
|
||||||
'rolling_on_the_floor_laughing',
|
|
||||||
'smiley',
|
|
||||||
'smile',
|
|
||||||
'sweat_smile',
|
|
||||||
'laughing',
|
|
||||||
'wink',
|
|
||||||
'blush',
|
|
||||||
'yum',
|
|
||||||
'sunglasses',
|
|
||||||
'heart_eyes',
|
|
||||||
'kissing_heart',
|
|
||||||
'kissing',
|
|
||||||
'kissing_smiling_eyes',
|
|
||||||
'kissing_closed_eyes',
|
|
||||||
'relaxed',
|
|
||||||
'slightly_smiling_face',
|
|
||||||
'hugging_face',
|
|
||||||
'grinning_face_with_star_eyes',
|
|
||||||
'thinking_face',
|
|
||||||
'face_with_one_eyebrow_raised',
|
|
||||||
'neutral_face',
|
|
||||||
'expressionless',
|
|
||||||
'no_mouth',
|
|
||||||
'face_with_rolling_eyes',
|
|
||||||
'smirk',
|
|
||||||
'persevere',
|
|
||||||
'disappointed_relieved',
|
|
||||||
'open_mouth',
|
|
||||||
'zipper_mouth_face',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### No Recents
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<util.ConversationContext theme={util.theme}>
|
|
||||||
<EmojiPicker
|
|
||||||
i18n={util.i18n}
|
|
||||||
onPickEmoji={e => console.log('onPickEmoji', e)}
|
|
||||||
onSetSkinTone={t => console.log('onSetSkinTone', t)}
|
|
||||||
onClose={() => console.log('onClose')}
|
|
||||||
recentEmojis={[]}
|
|
||||||
/>
|
|
||||||
</util.ConversationContext>
|
|
||||||
```
|
|
71
ts/components/emoji/EmojiPicker.stories.tsx
Normal file
71
ts/components/emoji/EmojiPicker.stories.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { EmojiPicker } from './EmojiPicker';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
storiesOf('Components/emojis/EmojiPicker', module)
|
||||||
|
.add('Base', () => {
|
||||||
|
return (
|
||||||
|
<EmojiPicker
|
||||||
|
i18n={i18n}
|
||||||
|
onPickEmoji={action('onPickEmoji')}
|
||||||
|
onSetSkinTone={action('onSetSkinTone')}
|
||||||
|
onClose={action('onClose')}
|
||||||
|
skinTone={0}
|
||||||
|
recentEmojis={[
|
||||||
|
'grinning',
|
||||||
|
'grin',
|
||||||
|
'joy',
|
||||||
|
'rolling_on_the_floor_laughing',
|
||||||
|
'smiley',
|
||||||
|
'smile',
|
||||||
|
'sweat_smile',
|
||||||
|
'laughing',
|
||||||
|
'wink',
|
||||||
|
'blush',
|
||||||
|
'yum',
|
||||||
|
'sunglasses',
|
||||||
|
'heart_eyes',
|
||||||
|
'kissing_heart',
|
||||||
|
'kissing',
|
||||||
|
'kissing_smiling_eyes',
|
||||||
|
'kissing_closed_eyes',
|
||||||
|
'relaxed',
|
||||||
|
'slightly_smiling_face',
|
||||||
|
'hugging_face',
|
||||||
|
'grinning_face_with_star_eyes',
|
||||||
|
'thinking_face',
|
||||||
|
'face_with_one_eyebrow_raised',
|
||||||
|
'neutral_face',
|
||||||
|
'expressionless',
|
||||||
|
'no_mouth',
|
||||||
|
'face_with_rolling_eyes',
|
||||||
|
'smirk',
|
||||||
|
'persevere',
|
||||||
|
'disappointed_relieved',
|
||||||
|
'open_mouth',
|
||||||
|
'zipper_mouth_face',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('No Recents', () => {
|
||||||
|
return (
|
||||||
|
<EmojiPicker
|
||||||
|
i18n={i18n}
|
||||||
|
onPickEmoji={action('onPickEmoji')}
|
||||||
|
onSetSkinTone={action('onSetSkinTone')}
|
||||||
|
onClose={action('onClose')}
|
||||||
|
skinTone={0}
|
||||||
|
recentEmojis={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
import { Emoji } from './Emoji';
|
import { Emoji } from './Emoji';
|
||||||
import { dataByCategory, search } from './lib';
|
import { dataByCategory, search } from './lib';
|
||||||
import { useRestoreFocus } from '../hooks';
|
import { useRestoreFocus } from '../../util/hooks';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
export type EmojiPickDataType = { skinTone?: number; shortName: string };
|
export type EmojiPickDataType = { skinTone?: number; shortName: string };
|
||||||
|
@ -29,7 +29,7 @@ export type OwnProps = {
|
||||||
readonly skinTone: number;
|
readonly skinTone: number;
|
||||||
readonly onSetSkinTone: (tone: number) => unknown;
|
readonly onSetSkinTone: (tone: number) => unknown;
|
||||||
readonly recentEmojis?: Array<string>;
|
readonly recentEmojis?: Array<string>;
|
||||||
readonly onClose: () => unknown;
|
readonly onClose?: () => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
|
||||||
|
@ -83,11 +83,15 @@ export const EmojiPicker = React.memo(
|
||||||
const [scrollToRow, setScrollToRow] = React.useState(0);
|
const [scrollToRow, setScrollToRow] = React.useState(0);
|
||||||
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
const [selectedTone, setSelectedTone] = React.useState(skinTone);
|
||||||
|
|
||||||
const handleToggleSearch = React.useCallback(() => {
|
const handleToggleSearch = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
setSelectedCategory(categories[0]);
|
setSelectedCategory(categories[0]);
|
||||||
setSearchMode(m => !m);
|
setSearchMode(m => !m);
|
||||||
}, [setSearchText, setSearchMode]);
|
},
|
||||||
|
[setSearchText, setSearchMode]
|
||||||
|
);
|
||||||
|
|
||||||
const debounceSearchChange = React.useMemo(
|
const debounceSearchChange = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -122,15 +126,16 @@ export const EmojiPicker = React.memo(
|
||||||
| React.KeyboardEvent<HTMLButtonElement>
|
| React.KeyboardEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
if ('key' in e) {
|
if ('key' in e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter' && doSend) {
|
||||||
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (doSend) {
|
|
||||||
doSend();
|
doSend();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const { shortName } = e.currentTarget.dataset;
|
const { shortName } = e.currentTarget.dataset;
|
||||||
if (shortName) {
|
if (shortName) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
onPickEmoji({ skinTone: selectedTone, shortName });
|
onPickEmoji({ skinTone: selectedTone, shortName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,7 +165,9 @@ export const EmojiPicker = React.memo(
|
||||||
' ', // Space
|
' ', // Space
|
||||||
].includes(event.key)
|
].includes(event.key)
|
||||||
) {
|
) {
|
||||||
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -224,8 +231,9 @@ export const EmojiPicker = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectCategory = React.useCallback(
|
const handleSelectCategory = React.useCallback(
|
||||||
({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const { category } = currentTarget.dataset;
|
e.stopPropagation();
|
||||||
|
const { category } = e.currentTarget.dataset;
|
||||||
if (category) {
|
if (category) {
|
||||||
setSelectedCategory(category);
|
setSelectedCategory(category);
|
||||||
setScrollToRow(catToRowOffsets[category]);
|
setScrollToRow(catToRowOffsets[category]);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
/* tslint:disable:cyclomatic-complexity */
|
/* tslint:disable:cyclomatic-complexity */
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRestoreFocus } from '../hooks';
|
import { useRestoreFocus } from '../../util/hooks';
|
||||||
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { StickerPackType } from '../../state/ducks/stickers';
|
import { StickerPackType } from '../../state/ducks/stickers';
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { useRestoreFocus } from '../hooks';
|
import { useRestoreFocus } from '../../util/hooks';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly onClose: () => unknown;
|
readonly onClose: () => unknown;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { take, uniq } from 'lodash';
|
import { take, uniq } from 'lodash';
|
||||||
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
|
import { useBoundActions } from '../../util/hooks';
|
||||||
|
|
||||||
const { updateEmojiUsage } = dataInterface;
|
const { updateEmojiUsage } = dataInterface;
|
||||||
|
|
||||||
|
@ -12,32 +13,34 @@ export type EmojisStateType = {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
type UseEmojiPayloadType = string;
|
type OnUseEmojiPayloadType = string;
|
||||||
type UseEmojiAction = {
|
type OnUseEmojiAction = {
|
||||||
type: 'emojis/USE_EMOJI';
|
type: 'emojis/USE_EMOJI';
|
||||||
payload: Promise<UseEmojiPayloadType>;
|
payload: Promise<OnUseEmojiPayloadType>;
|
||||||
};
|
};
|
||||||
type UseEmojiFulfilledAction = {
|
type OnUseEmojiFulfilledAction = {
|
||||||
type: 'emojis/USE_EMOJI_FULFILLED';
|
type: 'emojis/USE_EMOJI_FULFILLED';
|
||||||
payload: UseEmojiPayloadType;
|
payload: OnUseEmojiPayloadType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmojisActionType = UseEmojiAction | UseEmojiFulfilledAction;
|
export type EmojisActionType = OnUseEmojiAction | OnUseEmojiFulfilledAction;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
useEmoji,
|
onUseEmoji,
|
||||||
};
|
};
|
||||||
|
|
||||||
function useEmoji({ shortName }: EmojiPickDataType): UseEmojiAction {
|
export const useActions = () => useBoundActions(actions);
|
||||||
|
|
||||||
|
function onUseEmoji({ shortName }: EmojiPickDataType): OnUseEmojiAction {
|
||||||
return {
|
return {
|
||||||
type: 'emojis/USE_EMOJI',
|
type: 'emojis/USE_EMOJI',
|
||||||
payload: doUseEmoji(shortName),
|
payload: doUseEmoji(shortName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doUseEmoji(shortName: string): Promise<UseEmojiPayloadType> {
|
async function doUseEmoji(shortName: string): Promise<OnUseEmojiPayloadType> {
|
||||||
await updateEmojiUsage(shortName);
|
await updateEmojiUsage(shortName);
|
||||||
|
|
||||||
return shortName;
|
return shortName;
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
import * as storageShim from '../../shims/storage';
|
import * as storageShim from '../../shims/storage';
|
||||||
|
import { isShortName } from '../../components/emoji/lib';
|
||||||
|
import { useBoundActions } from '../../util/hooks';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -53,6 +58,8 @@ export const actions = {
|
||||||
resetItems,
|
resetItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useActions = () => useBoundActions(actions);
|
||||||
|
|
||||||
function putItem(key: string, value: any): ItemPutAction {
|
function putItem(key: string, value: any): ItemPutAction {
|
||||||
storageShim.put(key, value);
|
storageShim.put(key, value);
|
||||||
|
|
||||||
|
@ -123,3 +130,12 @@ export function reducer(
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
|
||||||
|
const selectRecentEmojis = createSelector(
|
||||||
|
({ emojis }: StateType) => emojis.recents,
|
||||||
|
recents => recents.filter(isShortName)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useRecentEmojis = () => useSelector(selectRecentEmojis);
|
||||||
|
|
|
@ -81,7 +81,7 @@ const dispatchPropsMap = {
|
||||||
mapDispatchToProps.removeItem('showStickersIntroduction'),
|
mapDispatchToProps.removeItem('showStickersIntroduction'),
|
||||||
clearShowPickerHint: () =>
|
clearShowPickerHint: () =>
|
||||||
mapDispatchToProps.removeItem('showStickerPickerHint'),
|
mapDispatchToProps.removeItem('showStickerPickerHint'),
|
||||||
onPickEmoji: mapDispatchToProps.useEmoji,
|
onPickEmoji: mapDispatchToProps.onUseEmoji,
|
||||||
};
|
};
|
||||||
|
|
||||||
const smart = connect(mapStateToProps, dispatchPropsMap);
|
const smart = connect(mapStateToProps, dispatchPropsMap);
|
||||||
|
|
57
ts/state/smart/EmojiPicker.tsx
Normal file
57
ts/state/smart/EmojiPicker.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { useActions as useItemActions, useRecentEmojis } from '../ducks/items';
|
||||||
|
import { useActions as useEmojiActions } from '../ducks/emojis';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmojiPicker,
|
||||||
|
Props as EmojiPickerProps,
|
||||||
|
} from '../../components/emoji/EmojiPicker';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
export const SmartEmojiPicker = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
Pick<EmojiPickerProps, 'onPickEmoji' | 'onClose' | 'style'>
|
||||||
|
>(({ onPickEmoji, onClose, style }, ref) => {
|
||||||
|
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||||
|
const skinTone = useSelector<StateType, number>(state =>
|
||||||
|
get(state, ['items', 'skinTone'], 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentEmojis = useRecentEmojis();
|
||||||
|
|
||||||
|
const { putItem } = useItemActions();
|
||||||
|
|
||||||
|
const onSetSkinTone = React.useCallback(
|
||||||
|
tone => {
|
||||||
|
putItem('skinTone', tone);
|
||||||
|
},
|
||||||
|
[putItem]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { onUseEmoji } = useEmojiActions();
|
||||||
|
|
||||||
|
const handlePickEmoji = React.useCallback(
|
||||||
|
data => {
|
||||||
|
onUseEmoji({ shortName: data.shortName });
|
||||||
|
onPickEmoji(data);
|
||||||
|
},
|
||||||
|
[onUseEmoji, onPickEmoji]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmojiPicker
|
||||||
|
ref={ref}
|
||||||
|
i18n={i18n}
|
||||||
|
skinTone={skinTone}
|
||||||
|
onSetSkinTone={onSetSkinTone}
|
||||||
|
onPickEmoji={handlePickEmoji}
|
||||||
|
recentEmojis={recentEmojis}
|
||||||
|
onClose={onClose}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { Timeline } from '../../components/conversation/Timeline';
|
import { Timeline } from '../../components/conversation/Timeline';
|
||||||
|
import { RenderEmojiPickerProps } from '../../components/conversation/ReactionPicker';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
@ -16,6 +17,7 @@ import { SmartTimelineItem } from './TimelineItem';
|
||||||
import { SmartTypingBubble } from './TypingBubble';
|
import { SmartTypingBubble } from './TypingBubble';
|
||||||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
|
import { SmartEmojiPicker } from './EmojiPicker';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
@ -41,6 +43,22 @@ function renderItem(
|
||||||
{...actionProps}
|
{...actionProps}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
id={messageId}
|
id={messageId}
|
||||||
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function renderEmojiPicker({
|
||||||
|
ref,
|
||||||
|
onPickEmoji,
|
||||||
|
onClose,
|
||||||
|
style,
|
||||||
|
}: RenderEmojiPickerProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<SmartEmojiPicker
|
||||||
|
ref={ref}
|
||||||
|
onPickEmoji={onPickEmoji}
|
||||||
|
onClose={onClose}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { ActionCreatorsMapObject, bindActionCreators } from 'redux';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
// Restore focus on teardown
|
// Restore focus on teardown
|
||||||
export const useRestoreFocus = (
|
export const useRestoreFocus = (
|
||||||
|
@ -28,3 +30,13 @@ export const useRestoreFocus = (
|
||||||
};
|
};
|
||||||
}, [focusRef, root]);
|
}, [focusRef, root]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useBoundActions = <T extends ActionCreatorsMapObject>(
|
||||||
|
actions: T
|
||||||
|
) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return React.useMemo(() => {
|
||||||
|
return bindActionCreators(actions, dispatch);
|
||||||
|
}, [dispatch]);
|
||||||
|
};
|
|
@ -11551,17 +11551,17 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||||
"lineNumber": 181,
|
"lineNumber": 184,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-04-16T19:36:47.586Z"
|
"updated": "2020-04-30T15:59:13.160Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 185,
|
"lineNumber": 188,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-04-16T19:36:47.586Z"
|
"updated": "2020-04-30T15:59:13.160Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
|
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -47,6 +47,9 @@ declare global {
|
||||||
ConversationController: ConversationControllerType;
|
ConversationController: ConversationControllerType;
|
||||||
WebAPI: WebAPIConnectType;
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
REACT_ANY_EMOJI: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue