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

@ -107,27 +107,18 @@
<div class='conversation-header'></div> <div class='conversation-header'></div>
<div class='main panel'> <div class='main panel'>
<div class='discussion-container'> <div class='discussion-container'>
<div class='bar-container hide'> <div class='bar-container hide'>
<div class='bar active progress-bar-striped progress-bar'></div> <div class='bar active progress-bar-striped progress-bar'></div>
</div> </div>
</div> </div>
<div class='bottom-bar' id='footer'> <div class='bottom-bar' id='footer'>
<div class='attachment-list'></div> <div class='compose'>
<div class='compose'> <form class='send clearfix file-input'>
<form class='send clearfix file-input'> <input type="file" class="file-input" multiple="multiple">
<div class='flex'> <div class='composition-area-placeholder'></div>
<div class='composition-area-placeholder'></div> </form>
<div class='capture-audio'> </div>
<button class='microphone'></button>
</div>
<div class='choose-file'>
<button class='paperclip thumbnail'></button>
<input type='file' class='file-input' multiple='multiple'>
</div>
</div>
</form>
</div>
</div> </div>
</div> </div>
</script> </script>

1
images/collapse-down.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>collapse-down-20</title><path d="M10,13.75a.746.746,0,0,1-.4-.114l-8-5A.75.75,0,1,1,2.4,7.364L10,12.116l7.6-4.752A.75.75,0,1,1,18.4,8.636l-8,5A.746.746,0,0,1,10,13.75Z"/></svg>

After

Width:  |  Height:  |  Size: 266 B

1
images/expand-up.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>expand-up-20</title><path d="M18,12.75a.746.746,0,0,1-.4-.114L10,7.884,2.4,12.636A.75.75,0,1,1,1.6,11.364l8-5a.748.748,0,0,1,.794,0l8,5A.75.75,0,0,1,18,12.75Z"/></svg>

After

Width:  |  Height:  |  Size: 257 B

1
images/send.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>send-solid-24</title><path d="M22.1,10.915,5.286,1.306A1.25,1.25,0,0,0,3.433,2.6L4.69,10.138,14,12,4.69,13.862,3.433,21.4a1.25,1.25,0,0,0,1.853,1.291L22.1,13.085A1.25,1.25,0,0,0,22.1,10.915Z"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -181,8 +181,11 @@
this.loadingScreen.$el.prependTo(this.$('.discussion-container')); this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
this.window = options.window; this.window = options.window;
const attachmentListEl = $(
'<div class="module-composition-area__attachment-list"></div>'
);
this.fileInput = new Whisper.FileInputView({ this.fileInput = new Whisper.FileInputView({
el: this.$('.attachment-list'), el: attachmentListEl,
}); });
this.listenTo( this.listenTo(
this.fileInput, this.fileInput,
@ -221,7 +224,7 @@
this.$('.send-message').blur(this.unfocusBottomBar.bind(this)); this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.setupHeader(); this.setupHeader();
this.setupCompositionArea(); this.setupCompositionArea({ attachmentListEl: attachmentListEl[0] });
}, },
events: { events: {
@ -316,20 +319,33 @@
this.$('.conversation-header').append(this.titleView.el); this.$('.conversation-header').append(this.titleView.el);
}, },
setupCompositionArea() { setupCompositionArea({ attachmentListEl }) {
const compositionApi = { current: null }; const compositionApi = { current: null };
this.compositionApi = compositionApi; this.compositionApi = compositionApi;
const micCellEl = $(`
<div class="capture-audio">
<button class="microphone"></button>
</div>
`)[0];
const attCellEl = $(`
<div class="choose-file">
<button class="paperclip thumbnail"></button>
</div>
`)[0];
const props = { const props = {
compositionApi, compositionApi,
onClickAddPack: () => this.showStickerManager(), onClickAddPack: () => this.showStickerManager(),
onPickSticker: (packId, stickerId) => onPickSticker: (packId, stickerId) =>
this.sendStickerMessage({ packId, stickerId }), this.sendStickerMessage({ packId, stickerId }),
onSubmit: message => this.sendMessage(message), onSubmit: message => this.sendMessage(message),
onDirtyChange: dirty => this.toggleMicrophone(dirty),
onEditorStateChange: (msg, caretLocation) => onEditorStateChange: (msg, caretLocation) =>
this.onEditorStateChange(msg, caretLocation), this.onEditorStateChange(msg, caretLocation),
onEditorSizeChange: rect => this.onEditorSizeChange(rect), onEditorSizeChange: rect => this.onEditorSizeChange(rect),
micCellEl,
attCellEl,
attachmentListEl,
}; };
this.compositionAreaView = new Whisper.ReactWrapperView({ this.compositionAreaView = new Whisper.ReactWrapperView({
@ -585,13 +601,10 @@
} }
}, },
toggleMicrophone(dirty = false) { toggleMicrophone() {
if (dirty || this.fileInput.hasFiles()) { this.compositionApi.current.setShowMic(!this.fileInput.hasFiles());
this.$('.capture-audio').hide();
} else {
this.$('.capture-audio').show();
}
}, },
captureAudio(e) { captureAudio(e) {
e.preventDefault(); e.preventDefault();
@ -617,6 +630,7 @@
view.on('send', this.handleAudioCapture.bind(this)); view.on('send', this.handleAudioCapture.bind(this));
view.on('closed', this.endCaptureAudio.bind(this)); view.on('closed', this.endCaptureAudio.bind(this));
view.$el.appendTo(this.$('.capture-audio')); view.$el.appendTo(this.$('.capture-audio'));
this.compositionApi.current.setMicActive(true);
this.disableMessageField(); this.disableMessageField();
this.$('.microphone').hide(); this.$('.microphone').hide();
@ -633,6 +647,7 @@
this.enableMessageField(); this.enableMessageField();
this.$('.microphone').show(); this.$('.microphone').show();
this.captureAudioView = null; this.captureAudioView = null;
this.compositionApi.current.setMicActive(false);
}, },
unfocusBottomBar() { unfocusBottomBar() {
@ -1808,7 +1823,8 @@
this.quoteView = new Whisper.ReactWrapperView({ this.quoteView = new Whisper.ReactWrapperView({
className: 'quote-wrapper', className: 'quote-wrapper',
Component: window.Signal.Components.Quote, Component: window.Signal.Components.Quote,
elCallback: el => this.$('.send').prepend(el), elCallback: el =>
this.$(this.compositionApi.current.attSlotRef.current).prepend(el),
props: Object.assign({}, props, { props: Object.assign({}, props, {
withContentAbove: true, withContentAbove: true,
onClose: () => { onClose: () => {
@ -2262,7 +2278,8 @@
this.previewView = new Whisper.ReactWrapperView({ this.previewView = new Whisper.ReactWrapperView({
className: 'preview-wrapper', className: 'preview-wrapper',
Component: window.Signal.Components.StagedLinkPreview, Component: window.Signal.Components.StagedLinkPreview,
elCallback: el => this.$('.send').prepend(el), elCallback: el =>
this.$(this.compositionApi.current.attSlotRef.current).prepend(el),
props, props,
onInitialRender: () => { onInitialRender: () => {
this.view.restoreBottomOffset(); this.view.restoreBottomOffset();

View file

@ -229,16 +229,15 @@
// things in the composition area. A margin on an inner div won't be included in that // things in the composition area. A margin on an inner div won't be included in that
// height calculation. // height calculation.
.bottom-bar .quote-wrapper { .bottom-bar .quote-wrapper {
margin-left: 37px; margin-left: 18px;
margin-right: 73px; margin-right: 18px;
margin-top: 3px; margin-top: 3px;
margin-bottom: -5px;
} }
.bottom-bar .preview-wrapper { .bottom-bar .preview-wrapper {
margin-top: 3px; margin-top: 3px;
margin-left: 37px; margin-left: 12px;
margin-right: 73px; margin-right: 12px;
margin-bottom: 2px; margin-bottom: 2px;
} }

View file

@ -111,16 +111,14 @@ a {
opacity: 0.5; opacity: 0.5;
border: none; border: none;
background: transparent; background: transparent;
margin-top: 2px;
&:before { &:before {
margin-top: 4px;
content: ''; content: '';
display: inline-block; display: inline-block;
width: $button-height; width: $button-height;
height: $button-height; height: $button-height;
@include color-svg('../images/paperclip.svg', $grey); @include color-svg('../images/paperclip.svg', $grey);
transform: rotateZ(-45deg); transform: rotateZ(-45deg) translateY(-2px);
} }
&:focus, &:focus,

View file

@ -829,10 +829,12 @@
// Module: Quoted Reply // Module: Quoted Reply
.module-quote-container { .module-quote-container {
margin-left: -6px; margin: {
margin-right: -6px; left: -6px;
margin-top: -4px; right: -6px;
margin-bottom: 5px; top: -4px;
bottom: 5px;
}
} }
.module-quote-container--with-content-above { .module-quote-container--with-content-above {
@ -2630,10 +2632,6 @@
// Module: Attachments // Module: Attachments
.module-attachments {
border-top: 1px solid $color-black-015;
}
.module-attachments__header { .module-attachments__header {
height: 24px; height: 24px;
position: relative; position: relative;
@ -2654,8 +2652,8 @@
.module-attachments__rail { .module-attachments__rail {
margin-top: 12px; margin-top: 12px;
margin-left: 16px; margin-left: 12px;
padding-right: 16px; padding-right: 12px;
overflow-x: scroll; overflow-x: scroll;
max-height: 142px; max-height: 142px;
white-space: nowrap; white-space: nowrap;
@ -4712,6 +4710,13 @@
min-height: 32px; min-height: 32px;
max-height: 80px; max-height: 80px;
overflow: auto; overflow: auto;
&--large {
max-height: 227px;
height: 227px;
.DraftEditor-root {
height: 227px - 2 * 7px; // subtract padding
}
}
} }
@include light-theme() { @include light-theme() {
@ -4808,11 +4813,35 @@
// Module: CompositionArea // Module: CompositionArea
.module-composition-area { .module-composition-area {
// Layout &__row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
&--center {
justify-content: center;
}
&--padded {
padding: 0 12px;
}
&--control-row {
margin-top: 8px;
}
&--column {
flex-direction: column;
}
&--show-on-focus {
opacity: 0;
transition: opacity 250ms ease-out;
}
}
$this: &;
&:focus-within,
&:hover {
#{$this}__row--show-on-focus {
opacity: 1;
}
}
// Child Elements
&__button-cell { &__button-cell {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -4820,13 +4849,60 @@
width: 44px; width: 44px;
height: 100%; height: 100%;
flex-shrink: 0; flex-shrink: 0;
&--microphone-active { &--mic-active {
width: 100px; width: 141px;
margin-right: 12px;
}
&--large-right {
margin-left: auto;
}
}
&__send-button {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
background: none;
border: none;
&:after {
display: block;
content: '';
width: 24px;
height: 24px;
flex-shrink: 0;
@include color-svg('../images/send.svg', $color-signal-blue);
} }
} }
&__input { &__input {
flex-grow: 1; flex-grow: 1;
} }
&__toggle-large {
width: 20px;
height: 20px;
border: none;
@include light-theme() {
@include color-svg('../images/expand-up.svg', $color-gray-45);
}
@include dark-theme() {
@include color-svg('../images/expand-up.svg', $color-gray-45);
}
&--large-active {
@include light-theme() {
@include color-svg('../images/collapse-down.svg', $color-gray-45);
}
@include dark-theme() {
@include color-svg('../images/collapse-down.svg', $color-gray-45);
}
}
}
&__attachment-list {
width: 100%;
}
} }
.composition-area-placeholder { .composition-area-placeholder {

View file

@ -2,14 +2,13 @@
text-align: center; text-align: center;
.microphone { .microphone {
height: 36px; height: 32px;
width: 36px; width: 32px;
text-align: center; text-align: center;
opacity: 0.5; opacity: 0.5;
background: transparent; background: transparent;
padding: 0; padding: 0;
border: none; border: none;
margin-top: 2px;
&:focus, &:focus,
&:hover { &:hover {
@ -26,18 +25,15 @@
} }
} }
.recorder { .recorder {
background: $color-white;
button { button {
float: right; float: right;
width: 36px; width: 32px;
height: 36px; height: 32px;
border-radius: 36px; border-radius: 32px;
margin-left: 5px; margin-left: 5px;
opacity: 0.5; opacity: 0.5;
text-align: center; text-align: center;
padding: 0; padding: 0;
margin-top: 5px;
&:focus, &:focus,
&:hover { &:hover {
@ -74,6 +70,7 @@
float: right; float: right;
line-height: 36px; line-height: 36px;
padding: 0 10px; padding: 0 10px;
transform: translateY(-2px);
@keyframes pulse { @keyframes pulse {
0% { 0% {

View file

@ -1433,10 +1433,6 @@ body.dark-theme {
// Module: Attachments // Module: Attachments
.module-attachments {
border-top: 1px solid $color-gray-75;
}
.module-attachments__close-button { .module-attachments__close-button {
@include color-svg('../images/x-16.svg', $color-gray-45); @include color-svg('../images/x-16.svg', $color-gray-45);
} }
@ -1694,8 +1690,6 @@ body.dark-theme {
} }
} }
.recorder { .recorder {
background: $color-black;
.finish { .finish {
background: lighten($color-core-green, 20%); background: lighten($color-core-green, 20%);
border: 1px solid $color-core-green; border: 1px solid $color-core-green;

View file

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

View file

@ -34,6 +34,7 @@ const colonsRegex = /(?:^|\s):[a-z0-9-_+]+:?/gi;
export type Props = { export type Props = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
readonly disabled?: boolean; readonly disabled?: boolean;
readonly large?: boolean;
readonly editorRef?: React.RefObject<Editor>; readonly editorRef?: React.RefObject<Editor>;
readonly inputApi?: React.MutableRefObject<InputApi | undefined>; readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
readonly skinTone?: EmojiPickDataType['skinTone']; readonly skinTone?: EmojiPickDataType['skinTone'];
@ -144,6 +145,7 @@ const combineRefs = createSelector(
export const CompositionInput = ({ export const CompositionInput = ({
i18n, i18n,
disabled, disabled,
large,
editorRef, editorRef,
inputApi, inputApi,
onDirtyChange, onDirtyChange,
@ -531,6 +533,10 @@ export const CompositionInput = ({
} }
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
if (large && !(e.ctrlKey || e.metaKey)) {
return getDefaultKeyBinding(e);
}
e.preventDefault(); e.preventDefault();
return 'submit'; return 'submit';
@ -562,7 +568,7 @@ export const CompositionInput = ({
return getDefaultKeyBinding(e); return getDefaultKeyBinding(e);
}, },
[emojiResults] [emojiResults, large]
); );
// Create popper root // Create popper root
@ -647,7 +653,14 @@ export const CompositionInput = ({
className="module-composition-input__input" className="module-composition-input__input"
ref={combineRefs(popperRef, measureRef, rootElRef)} ref={combineRefs(popperRef, measureRef, rootElRef)}
> >
<div className="module-composition-input__input__scroller"> <div
className={classNames(
'module-composition-input__input__scroller',
large
? 'module-composition-input__input__scroller--large'
: null
)}
>
<Editor <Editor
ref={editorRef} ref={editorRef}
editorState={editorRenderState} editorState={editorRenderState}

View file

@ -23,6 +23,7 @@ export type OwnProps = {
readonly clearShowIntroduction: () => unknown; readonly clearShowIntroduction: () => unknown;
readonly showPickerHint: boolean; readonly showPickerHint: boolean;
readonly clearShowPickerHint: () => unknown; readonly clearShowPickerHint: () => unknown;
readonly position?: 'top-end' | 'top-start';
}; };
export type Props = OwnProps; export type Props = OwnProps;
@ -44,6 +45,7 @@ export const StickerButton = React.memo(
clearShowIntroduction, clearShowIntroduction,
showPickerHint, showPickerHint,
clearShowPickerHint, clearShowPickerHint,
position = 'top-end',
}: Props) => { }: Props) => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>( const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
@ -188,7 +190,7 @@ export const StickerButton = React.memo(
)} )}
</Reference> </Reference>
{!open && !showIntroduction && installedPack ? ( {!open && !showIntroduction && installedPack ? (
<Popper placement="top-end" key={installedPack.id}> <Popper placement={position} key={installedPack.id}>
{({ ref, style, placement, arrowProps }) => ( {({ ref, style, placement, arrowProps }) => (
<div <div
ref={ref} ref={ref}
@ -225,7 +227,7 @@ export const StickerButton = React.memo(
</Popper> </Popper>
) : null} ) : null}
{!open && showIntroduction ? ( {!open && showIntroduction ? (
<Popper placement="top-end"> <Popper placement={position}>
{({ ref, style, placement, arrowProps }) => ( {({ ref, style, placement, arrowProps }) => (
<div <div
ref={ref} ref={ref}
@ -267,7 +269,7 @@ export const StickerButton = React.memo(
) : null} ) : null}
{open && popperRoot {open && popperRoot
? createPortal( ? createPortal(
<Popper placement="top-end"> <Popper placement={position}>
{({ ref, style }) => ( {({ ref, style }) => (
<StickerPicker <StickerPicker
ref={ref} ref={ref}

View file

@ -7885,5 +7885,23 @@
"lineNumber": 60, "lineNumber": 60,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2019-05-02T20:44:56.470Z" "updated": "2019-05-02T20:44:56.470Z"
},
{
"rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.js",
"line": " el.innerHTML = '';",
"lineNumber": 22,
"reasonCategory": "usageTrusted",
"updated": "2019-08-01T14:10:37.481Z",
"reasonDetail": "Our code, no user input, only clearing out the dom"
},
{
"rule": "DOM-innerHTML",
"path": "ts/components/CompositionArea.tsx",
"line": " el.innerHTML = '';",
"lineNumber": 64,
"reasonCategory": "usageTrusted",
"updated": "2019-08-01T14:10:37.481Z",
"reasonDetail": "Our code, no user input, only clearing out the dom"
} }
] ]