Large Message Composition
This commit is contained in:
parent
4d659f69cb
commit
79bba52cfb
|
@ -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
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.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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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% {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue