Large Message Composition
This commit is contained in:
parent
4d659f69cb
commit
79bba52cfb
14 changed files with 388 additions and 115 deletions
|
@ -107,27 +107,18 @@
|
|||
<div class='conversation-header'></div>
|
||||
<div class='main panel'>
|
||||
<div class='discussion-container'>
|
||||
<div class='bar-container hide'>
|
||||
<div class='bar active progress-bar-striped progress-bar'></div>
|
||||
</div>
|
||||
<div class='bar-container hide'>
|
||||
<div class='bar active progress-bar-striped progress-bar'></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='bottom-bar' id='footer'>
|
||||
<div class='attachment-list'></div>
|
||||
<div class='compose'>
|
||||
<form class='send clearfix file-input'>
|
||||
<div class='flex'>
|
||||
<div class='composition-area-placeholder'></div>
|
||||
<div class='capture-audio'>
|
||||
<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 class='compose'>
|
||||
<form class='send clearfix file-input'>
|
||||
<input type="file" class="file-input" multiple="multiple">
|
||||
<div class='composition-area-placeholder'></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
|
1
images/collapse-down.svg
Normal file
1
images/collapse-down.svg
Normal 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
1
images/expand-up.svg
Normal 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
1
images/send.svg
Normal 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 |
|
@ -181,8 +181,11 @@
|
|||
this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
|
||||
|
||||
this.window = options.window;
|
||||
const attachmentListEl = $(
|
||||
'<div class="module-composition-area__attachment-list"></div>'
|
||||
);
|
||||
this.fileInput = new Whisper.FileInputView({
|
||||
el: this.$('.attachment-list'),
|
||||
el: attachmentListEl,
|
||||
});
|
||||
this.listenTo(
|
||||
this.fileInput,
|
||||
|
@ -221,7 +224,7 @@
|
|||
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
|
||||
|
||||
this.setupHeader();
|
||||
this.setupCompositionArea();
|
||||
this.setupCompositionArea({ attachmentListEl: attachmentListEl[0] });
|
||||
},
|
||||
|
||||
events: {
|
||||
|
@ -316,20 +319,33 @@
|
|||
this.$('.conversation-header').append(this.titleView.el);
|
||||
},
|
||||
|
||||
setupCompositionArea() {
|
||||
setupCompositionArea({ attachmentListEl }) {
|
||||
const compositionApi = { current: null };
|
||||
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 = {
|
||||
compositionApi,
|
||||
onClickAddPack: () => this.showStickerManager(),
|
||||
onPickSticker: (packId, stickerId) =>
|
||||
this.sendStickerMessage({ packId, stickerId }),
|
||||
onSubmit: message => this.sendMessage(message),
|
||||
onDirtyChange: dirty => this.toggleMicrophone(dirty),
|
||||
onEditorStateChange: (msg, caretLocation) =>
|
||||
this.onEditorStateChange(msg, caretLocation),
|
||||
onEditorSizeChange: rect => this.onEditorSizeChange(rect),
|
||||
micCellEl,
|
||||
attCellEl,
|
||||
attachmentListEl,
|
||||
};
|
||||
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
|
@ -585,13 +601,10 @@
|
|||
}
|
||||
},
|
||||
|
||||
toggleMicrophone(dirty = false) {
|
||||
if (dirty || this.fileInput.hasFiles()) {
|
||||
this.$('.capture-audio').hide();
|
||||
} else {
|
||||
this.$('.capture-audio').show();
|
||||
}
|
||||
toggleMicrophone() {
|
||||
this.compositionApi.current.setShowMic(!this.fileInput.hasFiles());
|
||||
},
|
||||
|
||||
captureAudio(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -617,6 +630,7 @@
|
|||
view.on('send', this.handleAudioCapture.bind(this));
|
||||
view.on('closed', this.endCaptureAudio.bind(this));
|
||||
view.$el.appendTo(this.$('.capture-audio'));
|
||||
this.compositionApi.current.setMicActive(true);
|
||||
|
||||
this.disableMessageField();
|
||||
this.$('.microphone').hide();
|
||||
|
@ -633,6 +647,7 @@
|
|||
this.enableMessageField();
|
||||
this.$('.microphone').show();
|
||||
this.captureAudioView = null;
|
||||
this.compositionApi.current.setMicActive(false);
|
||||
},
|
||||
|
||||
unfocusBottomBar() {
|
||||
|
@ -1808,7 +1823,8 @@
|
|||
this.quoteView = new Whisper.ReactWrapperView({
|
||||
className: 'quote-wrapper',
|
||||
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, {
|
||||
withContentAbove: true,
|
||||
onClose: () => {
|
||||
|
@ -2262,7 +2278,8 @@
|
|||
this.previewView = new Whisper.ReactWrapperView({
|
||||
className: 'preview-wrapper',
|
||||
Component: window.Signal.Components.StagedLinkPreview,
|
||||
elCallback: el => this.$('.send').prepend(el),
|
||||
elCallback: el =>
|
||||
this.$(this.compositionApi.current.attSlotRef.current).prepend(el),
|
||||
props,
|
||||
onInitialRender: () => {
|
||||
this.view.restoreBottomOffset();
|
||||
|
|
|
@ -229,16 +229,15 @@
|
|||
// things in the composition area. A margin on an inner div won't be included in that
|
||||
// height calculation.
|
||||
.bottom-bar .quote-wrapper {
|
||||
margin-left: 37px;
|
||||
margin-right: 73px;
|
||||
margin-left: 18px;
|
||||
margin-right: 18px;
|
||||
margin-top: 3px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.bottom-bar .preview-wrapper {
|
||||
margin-top: 3px;
|
||||
margin-left: 37px;
|
||||
margin-right: 73px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,16 +111,14 @@ a {
|
|||
opacity: 0.5;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-top: 2px;
|
||||
|
||||
&:before {
|
||||
margin-top: 4px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: $button-height;
|
||||
height: $button-height;
|
||||
@include color-svg('../images/paperclip.svg', $grey);
|
||||
transform: rotateZ(-45deg);
|
||||
transform: rotateZ(-45deg) translateY(-2px);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
|
|
|
@ -829,10 +829,12 @@
|
|||
// Module: Quoted Reply
|
||||
|
||||
.module-quote-container {
|
||||
margin-left: -6px;
|
||||
margin-right: -6px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 5px;
|
||||
margin: {
|
||||
left: -6px;
|
||||
right: -6px;
|
||||
top: -4px;
|
||||
bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-quote-container--with-content-above {
|
||||
|
@ -2630,10 +2632,6 @@
|
|||
|
||||
// Module: Attachments
|
||||
|
||||
.module-attachments {
|
||||
border-top: 1px solid $color-black-015;
|
||||
}
|
||||
|
||||
.module-attachments__header {
|
||||
height: 24px;
|
||||
position: relative;
|
||||
|
@ -2654,8 +2652,8 @@
|
|||
|
||||
.module-attachments__rail {
|
||||
margin-top: 12px;
|
||||
margin-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-left: 12px;
|
||||
padding-right: 12px;
|
||||
overflow-x: scroll;
|
||||
max-height: 142px;
|
||||
white-space: nowrap;
|
||||
|
@ -4712,6 +4710,13 @@
|
|||
min-height: 32px;
|
||||
max-height: 80px;
|
||||
overflow: auto;
|
||||
&--large {
|
||||
max-height: 227px;
|
||||
height: 227px;
|
||||
.DraftEditor-root {
|
||||
height: 227px - 2 * 7px; // subtract padding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme() {
|
||||
|
@ -4808,11 +4813,35 @@
|
|||
|
||||
// Module: CompositionArea
|
||||
.module-composition-area {
|
||||
// Layout
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&__row {
|
||||
display: flex;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -4820,13 +4849,60 @@
|
|||
width: 44px;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
&--microphone-active {
|
||||
width: 100px;
|
||||
&--mic-active {
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
text-align: center;
|
||||
|
||||
.microphone {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin-top: 2px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
@ -26,18 +25,15 @@
|
|||
}
|
||||
}
|
||||
.recorder {
|
||||
background: $color-white;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 36px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
margin-left: 5px;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
margin-top: 5px;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
@ -74,6 +70,7 @@
|
|||
float: right;
|
||||
line-height: 36px;
|
||||
padding: 0 10px;
|
||||
transform: translateY(-2px);
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
|
|
|
@ -1433,10 +1433,6 @@ body.dark-theme {
|
|||
|
||||
// Module: Attachments
|
||||
|
||||
.module-attachments {
|
||||
border-top: 1px solid $color-gray-75;
|
||||
}
|
||||
|
||||
.module-attachments__close-button {
|
||||
@include color-svg('../images/x-16.svg', $color-gray-45);
|
||||
}
|
||||
|
@ -1694,8 +1690,6 @@ body.dark-theme {
|
|||
}
|
||||
}
|
||||
.recorder {
|
||||
background: $color-black;
|
||||
|
||||
.finish {
|
||||
background: lighten($color-core-green, 20%);
|
||||
border: 1px solid $color-core-green;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -34,6 +34,7 @@ const colonsRegex = /(?:^|\s):[a-z0-9-_+]+:?/gi;
|
|||
export type Props = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly disabled?: boolean;
|
||||
readonly large?: boolean;
|
||||
readonly editorRef?: React.RefObject<Editor>;
|
||||
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
|
||||
readonly skinTone?: EmojiPickDataType['skinTone'];
|
||||
|
@ -144,6 +145,7 @@ const combineRefs = createSelector(
|
|||
export const CompositionInput = ({
|
||||
i18n,
|
||||
disabled,
|
||||
large,
|
||||
editorRef,
|
||||
inputApi,
|
||||
onDirtyChange,
|
||||
|
@ -531,6 +533,10 @@ export const CompositionInput = ({
|
|||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (large && !(e.ctrlKey || e.metaKey)) {
|
||||
return getDefaultKeyBinding(e);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
return 'submit';
|
||||
|
@ -562,7 +568,7 @@ export const CompositionInput = ({
|
|||
|
||||
return getDefaultKeyBinding(e);
|
||||
},
|
||||
[emojiResults]
|
||||
[emojiResults, large]
|
||||
);
|
||||
|
||||
// Create popper root
|
||||
|
@ -647,7 +653,14 @@ export const CompositionInput = ({
|
|||
className="module-composition-input__input"
|
||||
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
|
||||
ref={editorRef}
|
||||
editorState={editorRenderState}
|
||||
|
|
|
@ -23,6 +23,7 @@ export type OwnProps = {
|
|||
readonly clearShowIntroduction: () => unknown;
|
||||
readonly showPickerHint: boolean;
|
||||
readonly clearShowPickerHint: () => unknown;
|
||||
readonly position?: 'top-end' | 'top-start';
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
@ -44,6 +45,7 @@ export const StickerButton = React.memo(
|
|||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
position = 'top-end',
|
||||
}: Props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
|
||||
|
@ -188,7 +190,7 @@ export const StickerButton = React.memo(
|
|||
)}
|
||||
</Reference>
|
||||
{!open && !showIntroduction && installedPack ? (
|
||||
<Popper placement="top-end" key={installedPack.id}>
|
||||
<Popper placement={position} key={installedPack.id}>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
|
@ -225,7 +227,7 @@ export const StickerButton = React.memo(
|
|||
</Popper>
|
||||
) : null}
|
||||
{!open && showIntroduction ? (
|
||||
<Popper placement="top-end">
|
||||
<Popper placement={position}>
|
||||
{({ ref, style, placement, arrowProps }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
|
@ -267,7 +269,7 @@ export const StickerButton = React.memo(
|
|||
) : null}
|
||||
{open && popperRoot
|
||||
? createPortal(
|
||||
<Popper placement="top-end">
|
||||
<Popper placement={position}>
|
||||
{({ ref, style }) => (
|
||||
<StickerPicker
|
||||
ref={ref}
|
||||
|
|
|
@ -7885,5 +7885,23 @@
|
|||
"lineNumber": 60,
|
||||
"reasonCategory": "falseMatch",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
Loading…
Add table
Reference in a new issue