New composition area with emoji typeahead
This commit is contained in:
parent
e62a1a7812
commit
7b645011c2
95 changed files with 1740 additions and 1293 deletions
|
@ -74,11 +74,10 @@ const {
|
|||
} = require('../../ts/components/conversation/VerificationNotification');
|
||||
|
||||
// State
|
||||
const { createEmojiButton } = require('../../ts/state/roots/createEmojiButton');
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const {
|
||||
createStickerButton,
|
||||
} = require('../../ts/state/roots/createStickerButton');
|
||||
createCompositionArea,
|
||||
} = require('../../ts/state/roots/createCompositionArea');
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const {
|
||||
createStickerManager,
|
||||
} = require('../../ts/state/roots/createStickerManager');
|
||||
|
@ -286,9 +285,8 @@ exports.setup = (options = {}) => {
|
|||
};
|
||||
|
||||
const Roots = {
|
||||
createEmojiButton,
|
||||
createCompositionArea,
|
||||
createLeftPane,
|
||||
createStickerButton,
|
||||
createStickerManager,
|
||||
createStickerPreviewModal,
|
||||
};
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
|
||||
this.listenTo(this.model, 'newmessage', this.addMessage);
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
this.listenTo(this.model, 'backgrounded', this.resetEmojiResults);
|
||||
this.listenTo(this.model, 'prune', this.onPrune);
|
||||
this.listenTo(this.model, 'unload', () => this.unload('model trigger'));
|
||||
this.listenTo(this.model, 'typing-update', this.renderTypingBubble);
|
||||
|
@ -200,11 +201,6 @@
|
|||
this.$('.discussion-container').append(this.view.el);
|
||||
this.view.render();
|
||||
|
||||
this.$messageField = this.$('.send-message');
|
||||
|
||||
this.onResize = this.forceUpdateMessageFieldSize.bind(this);
|
||||
this.window.addEventListener('resize', this.onResize);
|
||||
|
||||
this.onFocus = () => {
|
||||
if (this.$el.css('display') !== 'none') {
|
||||
this.markRead();
|
||||
|
@ -222,36 +218,22 @@
|
|||
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
|
||||
|
||||
this.setupHeader();
|
||||
this.setupEmojiPickerButton();
|
||||
this.setupStickerPickerButton();
|
||||
|
||||
this.lastSelectionStart = 0;
|
||||
document.addEventListener(
|
||||
'selectionchange',
|
||||
this.updateLastSelectionStart.bind(this, undefined)
|
||||
);
|
||||
this.setupCompositionArea();
|
||||
},
|
||||
|
||||
events: {
|
||||
'submit .send': 'clickSend',
|
||||
'input .send-message': 'updateMessageFieldSize',
|
||||
'keydown .send-message': 'updateMessageFieldSize',
|
||||
'keyup .send-message': 'onKeyUp',
|
||||
click: 'onClick',
|
||||
'click .emoji-button-placeholder': 'onClickPlaceholder',
|
||||
'click .sticker-button-placeholder': 'onClickPlaceholder',
|
||||
'click .composition-area-placeholder': 'onClickPlaceholder',
|
||||
'click .bottom-bar': 'focusMessageField',
|
||||
'click .capture-audio .microphone': 'captureAudio',
|
||||
'click .module-scroll-down': 'scrollToBottom',
|
||||
'focus .send-message': 'focusBottomBar',
|
||||
'change .file-input': 'toggleMicrophone',
|
||||
'blur .send-message': 'unfocusBottomBar',
|
||||
'loadMore .message-list': 'loadMoreMessages',
|
||||
'newOffscreenMessage .message-list': 'addScrollDownButtonWithCount',
|
||||
'atBottom .message-list': 'removeScrollDownButton',
|
||||
'farFromBottom .message-list': 'addScrollDownButton',
|
||||
'lazyScroll .message-list': 'onLazyScroll',
|
||||
'force-resize': 'forceUpdateMessageFieldSize',
|
||||
|
||||
'click button.paperclip': 'onChooseAttachment',
|
||||
'change input.file-input': 'onChoseAttachment',
|
||||
|
@ -331,52 +313,31 @@
|
|||
this.$('.conversation-header').append(this.titleView.el);
|
||||
},
|
||||
|
||||
setupEmojiPickerButton() {
|
||||
const props = {
|
||||
onForceSend: () => {
|
||||
this.sendMessage({});
|
||||
},
|
||||
onPickEmoji: e => this.insertEmoji(e),
|
||||
onClose: () => {
|
||||
const textarea = this.$messageField[0];
|
||||
|
||||
textarea.focus();
|
||||
|
||||
const newPos = textarea.value.length;
|
||||
textarea.selectionStart = newPos;
|
||||
textarea.selectionEnd = newPos;
|
||||
|
||||
this.forceUpdateLastSelectionStart(newPos);
|
||||
},
|
||||
};
|
||||
|
||||
this.emojiButtonView = new Whisper.ReactWrapperView({
|
||||
className: 'emoji-button-wrapper',
|
||||
JSX: Signal.State.Roots.createEmojiButton(window.reduxStore, props),
|
||||
});
|
||||
|
||||
// Finally, add it to the DOM
|
||||
this.$('.emoji-button-placeholder').append(this.emojiButtonView.el);
|
||||
},
|
||||
|
||||
setupStickerPickerButton() {
|
||||
if (!window.ENABLE_STICKER_SEND) {
|
||||
return;
|
||||
}
|
||||
setupCompositionArea() {
|
||||
const compositionApi = { current: null };
|
||||
this.compositionApi = compositionApi;
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
this.stickerButtonView = new Whisper.ReactWrapperView({
|
||||
className: 'sticker-button-wrapper',
|
||||
JSX: Signal.State.Roots.createStickerButton(window.reduxStore, props),
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
className: 'composition-area-wrapper',
|
||||
JSX: Signal.State.Roots.createCompositionArea(window.reduxStore, props),
|
||||
});
|
||||
|
||||
// Finally, add it to the DOM
|
||||
this.$('.sticker-button-placeholder').append(this.stickerButtonView.el);
|
||||
this.$('.composition-area-placeholder').append(
|
||||
this.compositionAreaView.el
|
||||
);
|
||||
},
|
||||
|
||||
// We need this, or clicking the reactified buttons will submit the form and send any
|
||||
|
@ -479,14 +440,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
this.window.removeEventListener('resize', this.onResize);
|
||||
this.window.removeEventListener('focus', this.onFocus);
|
||||
document.removeEventListener(
|
||||
'selectionchange',
|
||||
this.updateLastSelectionStart
|
||||
);
|
||||
|
||||
window.autosize.destroy(this.$messageField);
|
||||
|
||||
this.view.remove();
|
||||
|
||||
|
@ -628,11 +582,8 @@
|
|||
}
|
||||
},
|
||||
|
||||
toggleMicrophone() {
|
||||
if (
|
||||
this.$('.send-message').val().length > 0 ||
|
||||
this.fileInput.hasFiles()
|
||||
) {
|
||||
toggleMicrophone(dirty = false) {
|
||||
if (dirty || this.fileInput.hasFiles()) {
|
||||
this.$('.capture-audio').hide();
|
||||
} else {
|
||||
this.$('.capture-audio').show();
|
||||
|
@ -664,7 +615,7 @@
|
|||
view.on('closed', this.endCaptureAudio.bind(this));
|
||||
view.$el.appendTo(this.$('.capture-audio'));
|
||||
|
||||
this.$('.send-message').attr('disabled', true);
|
||||
this.disableMessageField();
|
||||
this.$('.microphone').hide();
|
||||
},
|
||||
handleAudioCapture(blob) {
|
||||
|
@ -673,10 +624,10 @@
|
|||
file: blob,
|
||||
isVoiceNote: true,
|
||||
});
|
||||
this.$('.bottom-bar form').submit();
|
||||
this.sendMessage();
|
||||
},
|
||||
endCaptureAudio() {
|
||||
this.$('.send-message').removeAttr('disabled');
|
||||
this.enableMessageField();
|
||||
this.$('.microphone').show();
|
||||
this.captureAudioView = null;
|
||||
},
|
||||
|
@ -745,7 +696,6 @@
|
|||
messagesLoaded.then(this.onLoaded.bind(this), this.onLoaded.bind(this));
|
||||
|
||||
this.view.resetScrollPosition();
|
||||
this.$el.trigger('force-resize');
|
||||
this.focusMessageField();
|
||||
this.renderTypingBubble();
|
||||
|
||||
|
@ -1088,13 +1038,28 @@
|
|||
return;
|
||||
}
|
||||
|
||||
this.$messageField.focus();
|
||||
const { compositionApi } = this;
|
||||
|
||||
if (compositionApi && compositionApi.current) {
|
||||
compositionApi.current.focusInput();
|
||||
}
|
||||
},
|
||||
|
||||
focusMessageFieldAndClearDisabled() {
|
||||
this.$messageField.removeAttr('disabled');
|
||||
this.$messageField.focus();
|
||||
this.updateLastSelectionStart();
|
||||
this.compositionApi.current.setDisabled(false);
|
||||
this.focusMessageField();
|
||||
},
|
||||
|
||||
disableMessageField() {
|
||||
this.compositionApi.current.setDisabled(true);
|
||||
},
|
||||
|
||||
enableMessageField() {
|
||||
this.compositionApi.current.setDisabled(false);
|
||||
},
|
||||
|
||||
resetEmojiResults() {
|
||||
this.compositionApi.current.resetEmojiResults(false);
|
||||
},
|
||||
|
||||
async loadMoreMessages() {
|
||||
|
@ -1648,7 +1613,6 @@
|
|||
view.remove();
|
||||
|
||||
if (this.panels.length === 0) {
|
||||
this.$el.trigger('force-resize');
|
||||
// Make sure poppers are positioned properly
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
@ -1716,36 +1680,6 @@
|
|||
});
|
||||
},
|
||||
|
||||
async clickSend(e, options) {
|
||||
e.preventDefault();
|
||||
|
||||
this.sendStart = Date.now();
|
||||
this.$messageField.attr('disabled', true);
|
||||
|
||||
try {
|
||||
const contacts = await this.getUntrustedContacts(options);
|
||||
|
||||
if (contacts && contacts.length) {
|
||||
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
||||
if (sendAnyway) {
|
||||
this.clickSend(e, { force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMessage(e);
|
||||
} catch (error) {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
window.log.error(
|
||||
'clickSend error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async sendStickerMessage(options = {}) {
|
||||
try {
|
||||
const contacts = await this.getUntrustedContacts(options);
|
||||
|
@ -1799,34 +1733,6 @@
|
|||
return null;
|
||||
},
|
||||
|
||||
insertEmoji({ shortName, skinTone }) {
|
||||
const skinReplacement = window.Signal.Emojis.hasVariation(
|
||||
shortName,
|
||||
skinTone
|
||||
)
|
||||
? `:skin-tone-${skinTone}:`
|
||||
: '';
|
||||
|
||||
const colons = `:${shortName}:${skinReplacement}`;
|
||||
|
||||
const textarea = this.$messageField[0];
|
||||
const hasFocus = document.activeElement === textarea;
|
||||
const startPos = hasFocus
|
||||
? textarea.selectionStart
|
||||
: this.lastSelectionStart;
|
||||
const endPos = hasFocus ? textarea.selectionEnd : this.lastSelectionStart;
|
||||
|
||||
textarea.value =
|
||||
textarea.value.substring(0, startPos) +
|
||||
colons +
|
||||
textarea.value.substring(endPos, textarea.value.length);
|
||||
const newPos = startPos + colons.length;
|
||||
textarea.selectionStart = newPos;
|
||||
textarea.selectionEnd = newPos;
|
||||
this.forceUpdateLastSelectionStart(newPos);
|
||||
this.forceUpdateMessageFieldSize({});
|
||||
},
|
||||
|
||||
async setQuoteMessage(messageId) {
|
||||
this.quote = null;
|
||||
this.quotedMessage = null;
|
||||
|
@ -1858,7 +1764,6 @@
|
|||
}
|
||||
if (!this.quotedMessage) {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1890,18 +1795,39 @@
|
|||
}),
|
||||
onInitialRender: () => {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async sendMessage(e) {
|
||||
async sendMessage(message = '', options = {}) {
|
||||
this.sendStart = Date.now();
|
||||
|
||||
try {
|
||||
const contacts = await this.getUntrustedContacts(options);
|
||||
this.disableMessageField();
|
||||
|
||||
if (contacts && contacts.length) {
|
||||
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
||||
if (sendAnyway) {
|
||||
this.sendMessage(message, { force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
window.log.error(
|
||||
'sendMessage error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeLastSeenIndicator();
|
||||
this.model.clearTypingTimers();
|
||||
|
||||
const input = this.$messageField;
|
||||
const message = window.Signal.Emojis.replaceColons(input.val()).trim();
|
||||
|
||||
let toast;
|
||||
if (extension.expired()) {
|
||||
toast = new Whisper.ExpiredToast();
|
||||
|
@ -1942,11 +1868,9 @@
|
|||
this.getLinkPreview()
|
||||
);
|
||||
|
||||
input.val('');
|
||||
this.compositionApi.current.reset();
|
||||
this.setQuoteMessage(null);
|
||||
this.resetLinkPreview();
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.forceUpdateMessageFieldSize(e);
|
||||
this.fileInput.clearAttachments();
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
|
@ -1958,24 +1882,16 @@
|
|||
}
|
||||
},
|
||||
|
||||
onKeyUp() {
|
||||
this.maybeBumpTyping();
|
||||
this.debouncedMaybeGrabLinkPreview();
|
||||
onEditorStateChange(messageText, caretLocation) {
|
||||
this.maybeBumpTyping(messageText);
|
||||
this.debouncedMaybeGrabLinkPreview(messageText, caretLocation);
|
||||
},
|
||||
|
||||
updateLastSelectionStart(newPos) {
|
||||
if (document.activeElement === this.$messageField[0]) {
|
||||
this.forceUpdateLastSelectionStart(newPos);
|
||||
}
|
||||
onEditorSizeChange() {
|
||||
this.view.scrollToBottomIfNeeded();
|
||||
},
|
||||
|
||||
forceUpdateLastSelectionStart(
|
||||
newPos = this.$messageField[0].selectionStart
|
||||
) {
|
||||
this.lastSelectionStart = newPos;
|
||||
},
|
||||
|
||||
maybeGrabLinkPreview() {
|
||||
maybeGrabLinkPreview(message, caretLocation) {
|
||||
// Don't generate link previews if user has turned them off
|
||||
if (!storage.get('linkPreviews', false)) {
|
||||
return;
|
||||
|
@ -1993,10 +1909,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const messageText = this.$messageField.val().trim();
|
||||
const caretLocation = this.$messageField.get(0).selectionStart;
|
||||
|
||||
if (!messageText) {
|
||||
if (!message) {
|
||||
this.resetLinkPreview();
|
||||
return;
|
||||
}
|
||||
|
@ -2005,7 +1918,7 @@
|
|||
}
|
||||
|
||||
const links = window.Signal.LinkPreviews.findLinks(
|
||||
messageText,
|
||||
message,
|
||||
caretLocation
|
||||
);
|
||||
const { currentlyMatchedLink } = this;
|
||||
|
@ -2310,7 +2223,6 @@
|
|||
}
|
||||
if (!this.currentlyMatchedLink) {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2332,7 +2244,6 @@
|
|||
props,
|
||||
onInitialRender: () => {
|
||||
this.view.restoreBottomOffset();
|
||||
this.updateMessageFieldSize({});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -2362,59 +2273,12 @@
|
|||
|
||||
// Called whenever the user changes the message composition field. But only
|
||||
// fires if there's content in the message field after the change.
|
||||
maybeBumpTyping() {
|
||||
const messageText = this.$messageField.val();
|
||||
maybeBumpTyping(messageText) {
|
||||
if (messageText.length) {
|
||||
this.model.throttledBumpTyping();
|
||||
}
|
||||
},
|
||||
|
||||
updateMessageFieldSize(event) {
|
||||
const keyCode = event.which || event.keyCode;
|
||||
|
||||
if (
|
||||
keyCode === 13 &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
!event.ctrlKey
|
||||
) {
|
||||
// enter pressed - submit the form now
|
||||
event.preventDefault();
|
||||
this.$('.bottom-bar form').submit();
|
||||
return;
|
||||
}
|
||||
this.toggleMicrophone();
|
||||
|
||||
this.view.measureScrollPosition();
|
||||
window.autosize(this.$messageField);
|
||||
|
||||
const $attachmentPreviews = this.$('.attachment-previews');
|
||||
const $bottomBar = this.$('.bottom-bar');
|
||||
const includeMargin = true;
|
||||
const quoteHeight = this.quoteView
|
||||
? this.quoteView.$el.outerHeight(includeMargin)
|
||||
: 0;
|
||||
|
||||
const height =
|
||||
this.$messageField.outerHeight() +
|
||||
$attachmentPreviews.outerHeight() +
|
||||
quoteHeight +
|
||||
parseInt($bottomBar.css('min-height'), 10);
|
||||
|
||||
$bottomBar.outerHeight(height);
|
||||
|
||||
this.view.scrollToBottomIfNeeded();
|
||||
},
|
||||
|
||||
forceUpdateMessageFieldSize(event) {
|
||||
if (this.isHidden()) {
|
||||
return;
|
||||
}
|
||||
this.view.scrollToBottomIfNeeded();
|
||||
window.autosize.update(this.$messageField);
|
||||
this.updateMessageFieldSize(event);
|
||||
},
|
||||
|
||||
isHidden() {
|
||||
return (
|
||||
this.$el.css('display') === 'none' ||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
Whisper.ConversationStack = Whisper.View.extend({
|
||||
className: 'conversation-stack',
|
||||
lastConversation: null,
|
||||
open(conversation) {
|
||||
const id = `conversation-${conversation.cid}`;
|
||||
if (id !== this.el.firstChild.id) {
|
||||
|
@ -42,6 +43,10 @@
|
|||
$el.prependTo(this.el);
|
||||
}
|
||||
conversation.trigger('opened');
|
||||
if (this.lastConversation) {
|
||||
this.lastConversation.trigger('backgrounded');
|
||||
}
|
||||
this.lastConversation = conversation;
|
||||
// Make sure poppers are positioned properly
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue