summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/composer/composer.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/composer/composer.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/composer/composer.js')
-rw-r--r--addons/mail/static/src/components/composer/composer.js444
1 files changed, 444 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
new file mode 100644
index 00000000..31654a4f
--- /dev/null
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -0,0 +1,444 @@
+odoo.define('mail/static/src/components/composer/composer.js', function (require) {
+'use strict';
+
+const components = {
+ AttachmentList: require('mail/static/src/components/attachment_list/attachment_list.js'),
+ ComposerSuggestedRecipientList: require('mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js'),
+ DropZone: require('mail/static/src/components/drop_zone/drop_zone.js'),
+ EmojisPopover: require('mail/static/src/components/emojis_popover/emojis_popover.js'),
+ FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'),
+ TextInput: require('mail/static/src/components/composer_text_input/composer_text_input.js'),
+ ThreadTextualTypingStatus: require('mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js'),
+};
+const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
+const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
+const {
+ isEventHandled,
+ markEventHandled,
+} = require('mail/static/src/utils/utils.js');
+
+const { Component } = owl;
+const { useRef } = owl.hooks;
+
+class Composer extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ this.isDropZoneVisible = useDragVisibleDropZone();
+ useShouldUpdateBasedOnProps({
+ compareDepth: {
+ textInputSendShortcuts: 1,
+ },
+ });
+ useStore(props => {
+ const composer = this.env.models['mail.composer'].get(props.composerLocalId);
+ const thread = composer && composer.thread;
+ return {
+ composer,
+ composerAttachments: composer ? composer.attachments : [],
+ composerCanPostMessage: composer && composer.canPostMessage,
+ composerHasFocus: composer && composer.hasFocus,
+ composerIsLog: composer && composer.isLog,
+ composerSubjectContent: composer && composer.subjectContent,
+ isDeviceMobile: this.env.messaging.device.isMobile,
+ thread,
+ threadChannelType: thread && thread.channel_type, // for livechat override
+ threadDisplayName: thread && thread.displayName,
+ threadMassMailing: thread && thread.mass_mailing,
+ threadModel: thread && thread.model,
+ threadName: thread && thread.name,
+ };
+ }, {
+ compareDepth: {
+ composerAttachments: 1,
+ },
+ });
+ useUpdate({ func: () => this._update() });
+ /**
+ * Reference of the emoji popover. Useful to include emoji popover as
+ * contained "inside" the composer.
+ */
+ this._emojisPopoverRef = useRef('emojisPopover');
+ /**
+ * Reference of the file uploader.
+ * Useful to programmatically prompts the browser file uploader.
+ */
+ this._fileUploaderRef = useRef('fileUploader');
+ /**
+ * Reference of the text input component.
+ */
+ this._textInputRef = useRef('textInput');
+ /**
+ * Reference of the subject input. Useful to set content.
+ */
+ this._subjectRef = useRef('subject');
+ this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
+ }
+
+ mounted() {
+ document.addEventListener('click', this._onClickCaptureGlobal, true);
+ }
+
+ willUnmount() {
+ document.removeEventListener('click', this._onClickCaptureGlobal, true);
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.composer}
+ */
+ get composer() {
+ return this.env.models['mail.composer'].get(this.props.composerLocalId);
+ }
+
+ /**
+ * Returns whether the given node is self or a children of self, including
+ * the emoji popover.
+ *
+ * @param {Node} node
+ * @returns {boolean}
+ */
+ contains(node) {
+ // emoji popover is outside but should be considered inside
+ const emojisPopover = this._emojisPopoverRef.comp;
+ if (emojisPopover && emojisPopover.contains(node)) {
+ return true;
+ }
+ return this.el.contains(node);
+ }
+
+ /**
+ * Get the current partner image URL.
+ *
+ * @returns {string}
+ */
+ get currentPartnerAvatar() {
+ const avatar = this.env.messaging.currentUser
+ ? this.env.session.url('/web/image', {
+ field: 'image_128',
+ id: this.env.messaging.currentUser.id,
+ model: 'res.users',
+ })
+ : '/web/static/src/img/user_menu_avatar.png';
+ return avatar;
+ }
+
+ /**
+ * Focus the composer.
+ */
+ focus() {
+ if (this.env.messaging.device.isMobile) {
+ this.el.scrollIntoView();
+ }
+ this._textInputRef.comp.focus();
+ }
+
+ /**
+ * Focusout the composer.
+ */
+ focusout() {
+ this._textInputRef.comp.focusout();
+ }
+
+ /**
+ * Determine whether composer should display a footer.
+ *
+ * @returns {boolean}
+ */
+ get hasFooter() {
+ return (
+ this.props.hasThreadTyping ||
+ this.composer.attachments.length > 0 ||
+ !this.props.isCompact
+ );
+ }
+
+ /**
+ * Determine whether the composer should display a header.
+ *
+ * @returns {boolean}
+ */
+ get hasHeader() {
+ return (
+ (this.props.hasThreadName && this.composer.thread) ||
+ (this.props.hasFollowers && !this.composer.isLog)
+ );
+ }
+
+ /**
+ * Get an object which is passed to FileUploader component to be used when
+ * creating attachment.
+ *
+ * @returns {Object}
+ */
+ get newAttachmentExtraData() {
+ return {
+ composers: [['replace', this.composer]],
+ };
+ }
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Post a message in the composer on related thread.
+ *
+ * Posting of the message could be aborted if it cannot be posted like if there are attachments
+ * currently uploading or if there is no text content and no attachments.
+ *
+ * @private
+ */
+ async _postMessage() {
+ if (!this.composer.canPostMessage) {
+ if (this.composer.hasUploadingAttachment) {
+ this.env.services['notification'].notify({
+ message: this.env._t("Please wait while the file is uploading."),
+ type: 'warning',
+ });
+ }
+ return;
+ }
+ await this.composer.postMessage();
+ // TODO: we might need to remove trigger and use the store to wait for the post rpc to be done
+ // task-2252858
+ this.trigger('o-message-posted');
+ }
+
+ /**
+ * @private
+ */
+ _update() {
+ if (this.props.isDoFocus) {
+ this.focus();
+ }
+ if (!this.composer) {
+ return;
+ }
+ if (this._subjectRef.el) {
+ this._subjectRef.el.value = this.composer.subjectContent;
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when clicking on attachment button.
+ *
+ * @private
+ */
+ _onClickAddAttachment() {
+ this._fileUploaderRef.comp.openBrowserFileUploader();
+ if (!this.env.device.isMobile) {
+ this.focus();
+ }
+ }
+
+ /**
+ * Discards the composer when clicking away.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickCaptureGlobal(ev) {
+ if (this.contains(ev.target)) {
+ return;
+ }
+ this.composer.discard();
+ }
+
+ /**
+ * Called when clicking on "expand" button.
+ *
+ * @private
+ */
+ _onClickFullComposer() {
+ this.composer.openFullComposer();
+ }
+
+ /**
+ * Called when clicking on "discard" button.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickDiscard(ev) {
+ this.composer.discard();
+ }
+
+ /**
+ * Called when clicking on "send" button.
+ *
+ * @private
+ */
+ _onClickSend() {
+ this._postMessage();
+ this.focus();
+ }
+
+ /**
+ * @private
+ */
+ _onComposerSuggestionClicked() {
+ this.focus();
+ }
+
+ /**
+ * @private
+ */
+ _onComposerTextInputSendShortcut() {
+ this._postMessage();
+ }
+
+ /**
+ * Called when some files have been dropped in the dropzone.
+ *
+ * @private
+ * @param {CustomEvent} ev
+ * @param {Object} ev.detail
+ * @param {FileList} ev.detail.files
+ */
+ async _onDropZoneFilesDropped(ev) {
+ ev.stopPropagation();
+ await this._fileUploaderRef.comp.uploadFiles(ev.detail.files);
+ this.isDropZoneVisible.value = false;
+ }
+
+ /**
+ * Called when selection an emoji from the emoji popover (from the emoji
+ * button).
+ *
+ * @private
+ * @param {CustomEvent} ev
+ * @param {Object} ev.detail
+ * @param {string} ev.detail.unicode
+ */
+ _onEmojiSelection(ev) {
+ ev.stopPropagation();
+ this._textInputRef.comp.saveStateInStore();
+ this.composer.insertIntoTextInput(ev.detail.unicode);
+ if (!this.env.device.isMobile) {
+ this.focus();
+ }
+ }
+
+ /**
+ * @private
+ */
+ _onInputSubject() {
+ this.composer.update({ subjectContent: this._subjectRef.el.value });
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydown(ev) {
+ if (ev.key === 'Escape') {
+ if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) {
+ return;
+ }
+ if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
+ return;
+ }
+ ev.preventDefault();
+ this.composer.discard();
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydownEmojiButton(ev) {
+ if (ev.key === 'Escape') {
+ if (this._emojisPopoverRef.comp) {
+ this._emojisPopoverRef.comp.close();
+ this.focus();
+ markEventHandled(ev, 'Composer.closeEmojisPopover');
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ async _onPasteTextInput(ev) {
+ if (!ev.clipboardData || !ev.clipboardData.files) {
+ return;
+ }
+ await this._fileUploaderRef.comp.uploadFiles(ev.clipboardData.files);
+ }
+
+}
+
+Object.assign(Composer, {
+ components,
+ defaultProps: {
+ hasCurrentPartnerAvatar: true,
+ hasDiscardButton: false,
+ hasFollowers: false,
+ hasSendButton: true,
+ hasThreadName: false,
+ hasThreadTyping: false,
+ isCompact: true,
+ isDoFocus: false,
+ isExpandable: false,
+ },
+ props: {
+ attachmentsDetailsMode: {
+ type: String,
+ optional: true,
+ },
+ composerLocalId: String,
+ hasCurrentPartnerAvatar: Boolean,
+ hasDiscardButton: Boolean,
+ hasFollowers: Boolean,
+ hasMentionSuggestionsBelowPosition: {
+ type: Boolean,
+ optional: true,
+ },
+ hasSendButton: Boolean,
+ hasThreadName: Boolean,
+ hasThreadTyping: Boolean,
+ /**
+ * Determines whether this should become focused.
+ */
+ isDoFocus: Boolean,
+ showAttachmentsExtensions: {
+ type: Boolean,
+ optional: true,
+ },
+ showAttachmentsFilenames: {
+ type: Boolean,
+ optional: true,
+ },
+ isCompact: Boolean,
+ isExpandable: Boolean,
+ /**
+ * If set, keyboard shortcuts from text input to send message.
+ * If not set, will use default values from `ComposerTextInput`.
+ */
+ textInputSendShortcuts: {
+ type: Array,
+ element: String,
+ optional: true,
+ },
+ },
+ template: 'mail.Composer',
+});
+
+return Composer;
+
+});