Large Message Composition

This commit is contained in:
Ken Powers 2019-08-06 15:18:37 -04:00 committed by Scott Nonnenberg
parent 4d659f69cb
commit 79bba52cfb
14 changed files with 388 additions and 115 deletions

View file

@ -1,5 +1,7 @@
import * as React from 'react';
import { Editor } from 'draft-js';
import { noop } from 'lodash';
import classNames from 'classnames';
import {
EmojiButton,
EmojiPickDataType,
@ -22,12 +24,21 @@ export type OwnProps = {
readonly compositionApi?: React.MutableRefObject<{
focusInput: () => void;
setDisabled: (disabled: boolean) => void;
setShowMic: (showMic: boolean) => void;
setMicActive: (micActive: boolean) => void;
attSlotRef: React.RefObject<HTMLDivElement>;
reset: InputApi['reset'];
resetEmojiResults: InputApi['resetEmojiResults'];
}>;
readonly micCellEl?: HTMLElement;
readonly attCellEl?: HTMLElement;
readonly attachmentListEl?: HTMLElement;
};
export type Props = CompositionInputProps &
export type Props = Pick<
CompositionInputProps,
'onSubmit' | 'onEditorSizeChange' | 'onEditorStateChange'
> &
Pick<
EmojiButtonProps,
'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
@ -48,11 +59,18 @@ export type Props = CompositionInputProps &
> &
OwnProps;
const emptyElement = (el: HTMLElement) => {
// tslint:disable-next-line no-inner-html
el.innerHTML = '';
};
// tslint:disable-next-line max-func-body-length
export const CompositionArea = ({
i18n,
attachmentListEl,
micCellEl,
attCellEl,
// CompositionInput
onDirtyChange,
onSubmit,
compositionApi,
onEditorSizeChange,
@ -76,16 +94,29 @@ export const CompositionArea = ({
clearShowPickerHint,
}: Props) => {
const [disabled, setDisabled] = React.useState(false);
const [showMic, setShowMic] = React.useState(true);
const [micActive, setMicActive] = React.useState(false);
const [dirty, setDirty] = React.useState(false);
const [large, setLarge] = React.useState(false);
const editorRef = React.useRef<Editor>(null);
const inputApiRef = React.useRef<InputApi | undefined>();
const handleForceSend = React.useCallback(
() => {
setLarge(false);
if (inputApiRef.current) {
inputApiRef.current.submit();
}
},
[inputApiRef]
[inputApiRef, setLarge]
);
const handleSubmit = React.useCallback<typeof onSubmit>(
(...args) => {
setLarge(false);
onSubmit(...args);
},
[setLarge, onSubmit]
);
const focusInput = React.useCallback(
@ -105,10 +136,16 @@ export const CompositionArea = ({
receivedPacks,
}) > 0;
// A ref to grab a slot where backbone can insert link previews and attachments
const attSlotRef = React.useRef<HTMLDivElement>(null);
if (compositionApi) {
compositionApi.current = {
focusInput,
setDisabled,
setShowMic,
setMicActive,
attSlotRef,
reset: () => {
if (inputApiRef.current) {
inputApiRef.current.reset();
@ -132,50 +169,178 @@ export const CompositionArea = ({
[inputApiRef, onPickEmoji]
);
const handleToggleLarge = React.useCallback(
() => {
setLarge(l => !l);
},
[setLarge]
);
// The following is a work-around to allow react to lay-out backbone-managed
// dom nodes until those functions are in React
const micCellRef = React.useRef<HTMLDivElement>(null);
const attCellRef = React.useRef<HTMLDivElement>(null);
React.useLayoutEffect(
() => {
const { current: micCellContainer } = micCellRef;
const { current: attCellContainer } = attCellRef;
if (micCellContainer && micCellEl) {
emptyElement(micCellContainer);
micCellContainer.appendChild(micCellEl);
}
if (attCellContainer && attCellEl) {
emptyElement(attCellContainer);
attCellContainer.appendChild(attCellEl);
}
return noop;
},
[micCellRef, attCellRef, micCellEl, attCellEl, large, dirty, showMic]
);
React.useLayoutEffect(
() => {
const { current: attSlot } = attSlotRef;
if (attSlot && attachmentListEl) {
attSlot.appendChild(attachmentListEl);
}
return noop;
},
[attSlotRef, attachmentListEl]
);
const emojiButtonFragment = (
<div className="module-composition-area__button-cell">
<EmojiButton
i18n={i18n}
doSend={handleForceSend}
onPickEmoji={insertEmoji}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
onClose={focusInput}
/>
</div>
);
const micButtonFragment = showMic ? (
<div
className={classNames(
'module-composition-area__button-cell',
micActive ? 'module-composition-area__button-cell--mic-active' : null,
large ? 'module-composition-area__button-cell--large-right' : null
)}
ref={micCellRef}
/>
) : null;
const attButtonFragment = (
<div className="module-composition-area__button-cell" ref={attCellRef} />
);
const sendButtonFragment = (
<div
className={classNames(
'module-composition-area__button-cell',
large ? 'module-composition-area__button-cell--large-right' : null
)}
>
<button
className="module-composition-area__send-button"
onClick={handleForceSend}
/>
</div>
);
const stickerButtonPlacement = large ? 'top-start' : 'top-end';
const stickerButtonFragment = withStickers ? (
<div className="module-composition-area__button-cell">
<StickerButton
i18n={i18n}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
installedPacks={installedPacks}
blessedPacks={blessedPacks}
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={onClickAddPack}
onPickSticker={onPickSticker}
clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint}
position={stickerButtonPlacement}
/>
</div>
) : null;
return (
<div className="module-composition-area">
<div className="module-composition-area__button-cell">
<EmojiButton
i18n={i18n}
doSend={handleForceSend}
onPickEmoji={insertEmoji}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
onClose={focusInput}
<div
className={classNames(
'module-composition-area__row',
'module-composition-area__row--center',
'module-composition-area__row--show-on-focus'
)}
>
<button
className={classNames(
'module-composition-area__toggle-large',
large ? 'module-composition-area__toggle-large--large-active' : null
)}
onClick={handleToggleLarge}
/>
</div>
<div className="module-composition-area__input">
<CompositionInput
i18n={i18n}
disabled={disabled}
editorRef={editorRef}
inputApi={inputApiRef}
onPickEmoji={onPickEmoji}
onSubmit={onSubmit}
onEditorSizeChange={onEditorSizeChange}
onEditorStateChange={onEditorStateChange}
onDirtyChange={onDirtyChange}
skinTone={skinTone}
/>
</div>
{withStickers ? (
<div className="module-composition-area__button-cell">
<StickerButton
<div
className={classNames(
'module-composition-area__row',
'module-composition-area__row--column'
)}
ref={attSlotRef}
/>
<div
className={classNames(
'module-composition-area__row',
large ? 'module-composition-area__row--padded' : null
)}
>
{!large ? emojiButtonFragment : null}
<div className="module-composition-area__input">
<CompositionInput
i18n={i18n}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
installedPacks={installedPacks}
blessedPacks={blessedPacks}
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={onClickAddPack}
onPickSticker={onPickSticker}
clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint}
disabled={disabled}
large={large}
editorRef={editorRef}
inputApi={inputApiRef}
onPickEmoji={onPickEmoji}
onSubmit={handleSubmit}
onEditorSizeChange={onEditorSizeChange}
onEditorStateChange={onEditorStateChange}
onDirtyChange={setDirty}
skinTone={skinTone}
/>
</div>
{!large ? (
<>
{stickerButtonFragment}
{!dirty ? micButtonFragment : null}
{attButtonFragment}
</>
) : null}
</div>
{large ? (
<div
className={classNames(
'module-composition-area__row',
'module-composition-area__row--control-row'
)}
>
{emojiButtonFragment}
{stickerButtonFragment}
{attButtonFragment}
{!dirty ? micButtonFragment : null}
{dirty || !showMic ? sendButtonFragment : null}
</div>
) : null}
</div>
);