Pre-alpha: React with any emoji, behind flag

This commit is contained in:
Ken Powers 2020-05-05 15:49:34 -04:00 committed by Scott Nonnenberg
parent d13c3d3350
commit 0865a5481c
31 changed files with 572 additions and 234 deletions

View file

@ -8,7 +8,14 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
// @ts-ignore
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);
@ -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]) =>
book.add(chapterTitle, () =>
propsArr.map(
@ -1294,6 +1316,7 @@ stories.forEach(([chapterTitle, propsArr]) =>
{...dataProps}
{...makeActionProps()}
{...makeHouseKeepingProps()}
renderEmojiPicker={renderEmojiPicker}
/>
</div>
</>

View file

@ -19,7 +19,7 @@ import {
OwnProps as ReactionViewerProps,
ReactionViewer,
} from './ReactionViewer';
import { ReactionPicker } from './ReactionPicker';
import { Props as ReactionPickerProps, ReactionPicker } from './ReactionPicker';
import { Emoji } from '../emoji/Emoji';
import {
@ -155,7 +155,10 @@ export type PropsActions = {
showExpiredOutgoingTapToViewToast: () => unknown;
};
export type Props = PropsData & PropsHousekeeping & PropsActions;
export type Props = PropsData &
PropsHousekeeping &
PropsActions &
Pick<ReactionPickerProps, 'renderEmojiPicker'>;
interface State {
expiring: boolean;
@ -1001,9 +1004,11 @@ export class Message extends React.PureComponent<Props, State> {
canReply,
direction,
disableMenu,
i18n,
id,
isSticker,
isTapToView,
renderEmojiPicker,
replyToMessage,
} = this.props;
@ -1121,6 +1126,7 @@ export class Message extends React.PureComponent<Props, State> {
<Popper placement="top">
{({ ref, style }) => (
<ReactionPicker
i18n={i18n}
ref={ref}
style={style}
selected={this.props.selectedReaction}
@ -1132,6 +1138,7 @@ export class Message extends React.PureComponent<Props, State> {
remove: emoji === this.props.selectedReaction,
});
}}
renderEmojiPicker={renderEmojiPicker}
/>
)}
</Popper>,

View file

@ -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>
```

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

View file

@ -1,20 +1,34 @@
import * as React from 'react';
import classNames from 'classnames';
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 = {
i18n: LocalizerType;
selected?: string;
onClose?: () => unknown;
onPick: (emoji: string) => unknown;
renderEmojiPicker: (props: RenderEmojiPickerProps) => React.ReactElement;
};
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
const emojis = ['❤️', '👍', '👎', '😂', '😮', '😢', '😡'];
const getEmojis = () =>
emojis.slice(0, window.REACT_ANY_EMOJI ? emojis.length - 1 : emojis.length);
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);
// Handle escape key
@ -32,12 +46,24 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
};
}, [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
useRestoreFocus(focusRef);
return (
<div {...rest} ref={ref} className="module-reaction-picker">
{emojis.map((emoji, index) => {
const otherSelected = selected && !getEmojis().includes(selected);
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;
return (
@ -55,6 +81,7 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
e.stopPropagation();
onPick(emoji);
}}
title={emoji}
>
<div className="module-reaction-picker__emoji-btn__emoji">
<Emoji size={48} emoji={emoji} />
@ -62,6 +89,31 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
</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>
);
}

View file

@ -4,7 +4,7 @@ import classNames from 'classnames';
import { ContactName } from './ContactName';
import { Avatar, Props as AvatarProps } from '../Avatar';
import { Emoji } from '../emoji/Emoji';
import { useRestoreFocus } from '../hooks';
import { useRestoreFocus } from '../../util/hooks';
export type Reaction = {
emoji: string;

View file

@ -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} />
```

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

View file

@ -3,6 +3,7 @@ import { LocalizerType } from '../../types/Util';
import {
Message,
Props as AllMessageProps,
PropsActions as MessageActionsType,
PropsData as MessageProps,
} from './Message';
@ -87,7 +88,9 @@ type PropsActionsType = MessageActionsType &
UnsupportedMessageActionsType &
SafetyNumberActionsType;
type PropsType = PropsLocalType & PropsActionsType;
export type PropsType = PropsLocalType &
PropsActionsType &
Pick<AllMessageProps, 'renderEmojiPicker'>;
export class TimelineItem extends React.PureComponent<PropsType> {
public render() {