Add emoji button to text story creation

This commit is contained in:
Josh Perez 2022-11-28 13:52:16 -05:00 committed by GitHub
parent d6d53f9d18
commit 77f92b6cc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 285 additions and 213 deletions

View file

@ -316,4 +316,10 @@
width: 24px; width: 24px;
} }
} }
&__emoji-button,
&__emoji-button::after {
height: 20px;
width: 20px;
}
} }

View file

@ -55,8 +55,13 @@ export default {
onDistributionListCreated: { action: true }, onDistributionListCreated: { action: true },
onHideMyStoriesFrom: { action: true }, onHideMyStoriesFrom: { action: true },
onSend: { action: true }, onSend: { action: true },
onSetSkinTone: { action: true },
onUseEmoji: { action: true },
onViewersUpdated: { action: true }, onViewersUpdated: { action: true },
processAttachment: { action: true }, processAttachment: { action: true },
recentEmojis: {
defaultValue: [],
},
recentStickers: { recentStickers: {
defaultValue: [], defaultValue: [],
}, },
@ -65,6 +70,9 @@ export default {
signalConnections: { signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation), defaultValue: Array.from(Array(42), getDefaultConversation),
}, },
skinTone: {
defaultValue: 0,
},
toggleSignalConnectionsModal: { action: true }, toggleSignalConnectionsModal: { action: true },
}, },
} as Meta; } as Meta;

View file

@ -15,6 +15,7 @@ import type { Props as StickerButtonProps } from './stickers/StickerButton';
import type { PropsType as SendStoryModalPropsType } from './SendStoryModal'; import type { PropsType as SendStoryModalPropsType } from './SendStoryModal';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
import type { imageToBlurHash } from '../util/imageToBlurHash'; import type { imageToBlurHash } from '../util/imageToBlurHash';
import type { PropsType as TextStoryCreatorPropsType } from './TextStoryCreator';
import { TEXT_ATTACHMENT } from '../types/MIME'; import { TEXT_ATTACHMENT } from '../types/MIME';
import { isVideoAttachment } from '../types/Attachment'; import { isVideoAttachment } from '../types/Attachment';
@ -70,6 +71,10 @@ export type PropsType = {
| 'toggleGroupsForStorySend' | 'toggleGroupsForStorySend'
| 'mostRecentActiveStoryTimestampByGroupOrDistributionList' | 'mostRecentActiveStoryTimestampByGroupOrDistributionList'
| 'toggleSignalConnectionsModal' | 'toggleSignalConnectionsModal'
> &
Pick<
TextStoryCreatorPropsType,
'onUseEmoji' | 'skinTone' | 'onSetSkinTone' | 'recentEmojis'
>; >;
export function StoryCreator({ export function StoryCreator({
@ -87,7 +92,7 @@ export function StoryCreator({
isSending, isSending,
linkPreview, linkPreview,
me, me,
ourConversationId, mostRecentActiveStoryTimestampByGroupOrDistributionList,
onClose, onClose,
onDeleteList, onDeleteList,
onDistributionListCreated, onDistributionListCreated,
@ -96,15 +101,19 @@ export function StoryCreator({
onRepliesNReactionsChanged, onRepliesNReactionsChanged,
onSelectedStoryList, onSelectedStoryList,
onSend, onSend,
onSetSkinTone,
onUseEmoji,
onViewersUpdated, onViewersUpdated,
ourConversationId,
processAttachment, processAttachment,
recentEmojis,
recentStickers, recentStickers,
renderCompositionTextArea, renderCompositionTextArea,
sendStoryModalOpenStateChanged, sendStoryModalOpenStateChanged,
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
signalConnections, signalConnections,
skinTone,
toggleGroupsForStorySend, toggleGroupsForStorySend,
mostRecentActiveStoryTimestampByGroupOrDistributionList,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [draftAttachment, setDraftAttachment] = useState< const [draftAttachment, setDraftAttachment] = useState<
@ -236,6 +245,10 @@ export function StoryCreator({
}); });
setIsReadyToSend(true); setIsReadyToSend(true);
}} }}
onUseEmoji={onUseEmoji}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
skinTone={skinTone}
/> />
)} )}
</> </>

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure'; import Measure from 'react-measure';
import React, { useEffect, useRef, useState } from 'react'; import React, { forwardRef, useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import classNames from 'classnames'; import classNames from 'classnames';
@ -21,6 +21,7 @@ import {
getBackgroundColor, getBackgroundColor,
} from '../util/getStoryBackground'; } from '../util/getStoryBackground';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { useRefMerger } from '../hooks/useRefMerger';
const renderNewLines: RenderTextCallbackType = ({ const renderNewLines: RenderTextCallbackType = ({
text: textWithNewLines, text: textWithNewLines,
@ -105,7 +106,9 @@ function getTextStyles(
}; };
} }
export function TextAttachment({ export const TextAttachment = forwardRef<HTMLTextAreaElement, PropsType>(
function TextAttachmentForwarded(
{
disableLinkPreviewPopup, disableLinkPreviewPopup,
i18n, i18n,
isEditingText, isEditingText,
@ -114,18 +117,20 @@ export function TextAttachment({
onClick, onClick,
onRemoveLinkPreview, onRemoveLinkPreview,
textAttachment, textAttachment,
}: PropsType): JSX.Element | null { },
forwardedTextEditorRef
): JSX.Element | null {
const linkPreview = useRef<HTMLDivElement | null>(null); const linkPreview = useRef<HTMLDivElement | null>(null);
const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState< const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState<
number | undefined number | undefined
>(); >();
const textContent = textAttachment.text || ''; const textContent = textAttachment.text || '';
const textEditorRef = useRef<HTMLTextAreaElement | null>(null); const textEditorRef = useRef<HTMLTextAreaElement | null>(null);
const refMerger = useRefMerger();
useEffect(() => { useEffect(() => {
const node = textEditorRef.current; const node = textEditorRef?.current;
if (!node) { if (!node) {
return; return;
} }
@ -241,7 +246,7 @@ export function TextAttachment({
disabled={!isEditingText} disabled={!isEditingText}
onChange={ev => onChange(ev.currentTarget.value)} onChange={ev => onChange(ev.currentTarget.value)}
placeholder={i18n('TextAttachment__placeholder')} placeholder={i18n('TextAttachment__placeholder')}
ref={textEditorRef} ref={refMerger(forwardedTextEditorRef, textEditorRef)}
style={getTextStyles( style={getTextStyles(
textContent, textContent,
textAttachment.textForegroundColor, textAttachment.textForegroundColor,
@ -284,7 +289,9 @@ export function TextAttachment({
{onRemoveLinkPreview && ( {onRemoveLinkPreview && (
<div className="TextAttachment__preview__remove"> <div className="TextAttachment__preview__remove">
<button <button
aria-label={i18n('Keyboard--remove-draft-link-preview')} aria-label={i18n(
'Keyboard--remove-draft-link-preview'
)}
type="button" type="button"
onClick={onRemoveLinkPreview} onClick={onRemoveLinkPreview}
/> />
@ -309,3 +316,4 @@ export function TextAttachment({
</Measure> </Measure>
); );
} }
);

View file

@ -7,12 +7,15 @@ import classNames from 'classnames';
import { get, has, noop } from 'lodash'; import { get, has, noop } from 'lodash';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { LinkPreviewType } from '../types/message/LinkPreviews'; import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { Props as EmojiButtonPropsType } from './emoji/EmojiButton';
import type { TextAttachmentType } from '../types/Attachment'; import type { TextAttachmentType } from '../types/Attachment';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonVariant } from './Button';
import { ContextMenu } from './ContextMenu'; import { ContextMenu } from './ContextMenu';
import { EmojiButton } from './emoji/EmojiButton';
import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview'; import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import type { MaybeGrabLinkPreviewOptionsType } from '../types/LinkPreview'; import type { MaybeGrabLinkPreviewOptionsType } from '../types/LinkPreview';
import { Input } from './Input'; import { Input } from './Input';
@ -26,6 +29,7 @@ import {
COLOR_WHITE_INT, COLOR_WHITE_INT,
getBackgroundColor, getBackgroundColor,
} from '../util/getStoryBackground'; } from '../util/getStoryBackground';
import { convertShortName } from './emoji/lib';
import { objectMap } from '../util/objectMap'; import { objectMap } from '../util/objectMap';
import { handleOutsideClick } from '../util/handleOutsideClick'; import { handleOutsideClick } from '../util/handleOutsideClick';
import { ConfirmDiscardDialog } from './ConfirmDiscardDialog'; import { ConfirmDiscardDialog } from './ConfirmDiscardDialog';
@ -42,7 +46,8 @@ export type PropsType = {
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
onClose: () => unknown; onClose: () => unknown;
onDone: (textAttachment: TextAttachmentType) => unknown; onDone: (textAttachment: TextAttachmentType) => unknown;
}; onUseEmoji: (_: EmojiPickDataType) => unknown;
} & Pick<EmojiButtonPropsType, 'onSetSkinTone' | 'recentEmojis' | 'skinTone'>;
enum LinkPreviewApplied { enum LinkPreviewApplied {
None = 'None', None = 'None',
@ -128,6 +133,10 @@ export function TextStoryCreator({
linkPreview, linkPreview,
onClose, onClose,
onDone, onDone,
onSetSkinTone,
onUseEmoji,
recentEmojis,
skinTone,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [showConfirmDiscardModal, setShowConfirmDiscardModal] = useState(false); const [showConfirmDiscardModal, setShowConfirmDiscardModal] = useState(false);
@ -145,16 +154,6 @@ export function TextStoryCreator({
const [sliderValue, setSliderValue] = useState<number>(100); const [sliderValue, setSliderValue] = useState<number>(100);
const [text, setText] = useState<string>(''); const [text, setText] = useState<string>('');
const textEditorRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isEditingText) {
textEditorRef.current?.focus();
} else {
textEditorRef.current?.blur();
}
}, [isEditingText]);
const [isColorPickerShowing, setIsColorPickerShowing] = useState(false); const [isColorPickerShowing, setIsColorPickerShowing] = useState(false);
const [colorPickerPopperButtonRef, setColorPickerPopperButtonRef] = const [colorPickerPopperButtonRef, setColorPickerPopperButtonRef] =
useState<HTMLButtonElement | null>(null); useState<HTMLButtonElement | null>(null);
@ -328,6 +327,8 @@ export function TextStoryCreator({
const hasChanges = Boolean(text || hasLinkPreviewApplied); const hasChanges = Boolean(text || hasLinkPreviewApplied);
const textEditorRef = useRef<HTMLTextAreaElement | null>(null);
return ( return (
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}> <FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="StoryCreator"> <div className="StoryCreator">
@ -345,6 +346,7 @@ export function TextStoryCreator({
onRemoveLinkPreview={() => { onRemoveLinkPreview={() => {
setLinkPreviewApplied(LinkPreviewApplied.None); setLinkPreviewApplied(LinkPreviewApplied.None);
}} }}
ref={textEditorRef}
textAttachment={textAttachment} textAttachment={textAttachment}
/> />
</div> </div>
@ -428,6 +430,26 @@ export function TextStoryCreator({
}} }}
type="button" type="button"
/> />
<EmojiButton
className="StoryCreator__emoji-button"
i18n={i18n}
onPickEmoji={data => {
onUseEmoji(data);
const emoji = convertShortName(data.shortName, data.skinTone);
const insertAt =
textEditorRef.current?.selectionEnd ?? text.length;
setText(
originalText =>
`${originalText.substr(
0,
insertAt
)}${emoji}${originalText.substr(insertAt, text.length)}`
);
}}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
/>
</div> </div>
) : ( ) : (
<div className="StoryCreator__toolbar--space" /> <div className="StoryCreator__toolbar--space" />

View file

@ -7,6 +7,7 @@ import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { LinkPreviewSourceType } from '../../types/LinkPreview'; import { LinkPreviewSourceType } from '../../types/LinkPreview';
import { SmartCompositionTextArea } from './CompositionTextArea';
import { StoryCreator } from '../../components/StoryCreator'; import { StoryCreator } from '../../components/StoryCreator';
import { import {
getAllSignalConnections, getAllSignalConnections,
@ -22,18 +23,23 @@ import {
getInstalledStickerPacks, getInstalledStickerPacks,
getRecentStickers, getRecentStickers,
} from '../selectors/stickers'; } from '../selectors/stickers';
import { getHasSetMyStoriesPrivacy } from '../selectors/items'; import { getAddStoryData } from '../selectors/stories';
import {
getEmojiSkinTone,
getHasSetMyStoriesPrivacy,
} from '../selectors/items';
import { getLinkPreview } from '../selectors/linkPreviews'; import { getLinkPreview } from '../selectors/linkPreviews';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { processAttachment } from '../../util/processAttachment';
import { imageToBlurHash } from '../../util/imageToBlurHash'; import { imageToBlurHash } from '../../util/imageToBlurHash';
import { processAttachment } from '../../util/processAttachment';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useActions as useEmojisActions } from '../ducks/emojis';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useActions as useItemsActions } from '../ducks/items';
import { useLinkPreviewActions } from '../ducks/linkPreviews'; import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { useRecentEmojis } from '../selectors/emojis';
import { useStoriesActions } from '../ducks/stories'; import { useStoriesActions } from '../ducks/stories';
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
import { SmartCompositionTextArea } from './CompositionTextArea';
import { getAddStoryData } from '../selectors/stories';
export type PropsType = { export type PropsType = {
file?: File; file?: File;
@ -81,6 +87,11 @@ export function SmartStoryCreator(): JSX.Element | null {
const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined; const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined;
const isSending = addStoryData?.sending || false; const isSending = addStoryData?.sending || false;
const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
const { onSetSkinTone } = useItemsActions();
const { onUseEmoji } = useEmojisActions();
return ( return (
<StoryCreator <StoryCreator
candidateConversations={candidateConversations} candidateConversations={candidateConversations}
@ -97,7 +108,9 @@ export function SmartStoryCreator(): JSX.Element | null {
isSending={isSending} isSending={isSending}
linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)} linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
me={me} me={me}
ourConversationId={ourConversationId} mostRecentActiveStoryTimestampByGroupOrDistributionList={
mostRecentActiveStoryTimestampByGroupOrDistributionList
}
onClose={() => setAddStoryData(undefined)} onClose={() => setAddStoryData(undefined)}
onDeleteList={deleteDistributionList} onDeleteList={deleteDistributionList}
onDistributionListCreated={createDistributionList} onDistributionListCreated={createDistributionList}
@ -106,17 +119,19 @@ export function SmartStoryCreator(): JSX.Element | null {
onRepliesNReactionsChanged={allowsRepliesChanged} onRepliesNReactionsChanged={allowsRepliesChanged}
onSelectedStoryList={verifyStoryListMembers} onSelectedStoryList={verifyStoryListMembers}
onSend={sendStoryMessage} onSend={sendStoryMessage}
onSetSkinTone={onSetSkinTone}
onUseEmoji={onUseEmoji}
onViewersUpdated={updateStoryViewers} onViewersUpdated={updateStoryViewers}
ourConversationId={ourConversationId}
processAttachment={processAttachment} processAttachment={processAttachment}
recentEmojis={recentEmojis}
recentStickers={recentStickers} recentStickers={recentStickers}
renderCompositionTextArea={SmartCompositionTextArea} renderCompositionTextArea={SmartCompositionTextArea}
sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged} sendStoryModalOpenStateChanged={sendStoryModalOpenStateChanged}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
signalConnections={signalConnections} signalConnections={signalConnections}
skinTone={skinTone}
toggleGroupsForStorySend={toggleGroupsForStorySend} toggleGroupsForStorySend={toggleGroupsForStorySend}
mostRecentActiveStoryTimestampByGroupOrDistributionList={
mostRecentActiveStoryTimestampByGroupOrDistributionList
}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/> />
); );

View file

@ -9093,7 +9093,7 @@
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/TextStoryCreator.tsx", "path": "ts/components/TextStoryCreator.tsx",
"line": " const textEditorRef = useRef<HTMLInputElement | null>(null);", "line": " const textEditorRef = useRef<HTMLTextAreaElement | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-06-16T23:23:32.306Z" "updated": "2022-06-16T23:23:32.306Z"
}, },