diff options
Diffstat (limited to 'addons/mail/static/src/components/chat_window/chat_window.js')
| -rw-r--r-- | addons/mail/static/src/components/chat_window/chat_window.js | 363 |
1 files changed, 363 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js new file mode 100644 index 00000000..f9271523 --- /dev/null +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -0,0 +1,363 @@ +odoo.define('mail/static/src/components/chat_window/chat_window.js', function (require) { +'use strict'; + +const components = { + AutocompleteInput: require('mail/static/src/components/autocomplete_input/autocomplete_input.js'), + ChatWindowHeader: require('mail/static/src/components/chat_window_header/chat_window_header.js'), + ThreadView: require('mail/static/src/components/thread_view/thread_view.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 } = require('mail/static/src/utils/utils.js'); + +const patchMixin = require('web.patchMixin'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class ChatWindow extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const chatWindow = this.env.models['mail.chat_window'].get(props.chatWindowLocalId); + const thread = chatWindow ? chatWindow.thread : undefined; + return { + chatWindow, + chatWindowHasNewMessageForm: chatWindow && chatWindow.hasNewMessageForm, + chatWindowIsDoFocus: chatWindow && chatWindow.isDoFocus, + chatWindowIsFocused: chatWindow && chatWindow.isFocused, + chatWindowIsFolded: chatWindow && chatWindow.isFolded, + chatWindowThreadView: chatWindow && chatWindow.threadView, + chatWindowVisibleIndex: chatWindow && chatWindow.visibleIndex, + chatWindowVisibleOffset: chatWindow && chatWindow.visibleOffset, + isDeviceMobile: this.env.messaging.device.isMobile, + localeTextDirection: this.env.messaging.locale.textDirection, + thread, + threadMassMailing: thread && thread.mass_mailing, + threadModel: thread && thread.model, + }; + }); + useUpdate({ func: () => this._update() }); + /** + * Reference of the header of the chat window. + * Useful to prevent click on header from wrongly focusing the window. + */ + this._chatWindowHeaderRef = useRef('header'); + /** + * Reference of the autocomplete input (new_message chat window only). + * Useful when focusing this chat window, which consists of focusing + * this input. + */ + this._inputRef = useRef('input'); + /** + * Reference of thread in the chat window (chat window with thread + * only). Useful when focusing this chat window, which consists of + * focusing this thread. Will likely focus the composer of thread, if + * it has one! + */ + this._threadRef = useRef('thread'); + this._onWillHideHomeMenu = this._onWillHideHomeMenu.bind(this); + this._onWillShowHomeMenu = this._onWillShowHomeMenu.bind(this); + // the following are passed as props to children + this._onAutocompleteSelect = this._onAutocompleteSelect.bind(this); + this._onAutocompleteSource = this._onAutocompleteSource.bind(this); + this._constructor(...args); + } + + /** + * Allows patching constructor. + */ + _constructor() {} + + mounted() { + this.env.messagingBus.on('will_hide_home_menu', this, this._onWillHideHomeMenu); + this.env.messagingBus.on('will_show_home_menu', this, this._onWillShowHomeMenu); + } + + willUnmount() { + this.env.messagingBus.off('will_hide_home_menu', this, this._onWillHideHomeMenu); + this.env.messagingBus.off('will_show_home_menu', this, this._onWillShowHomeMenu); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.chat_window} + */ + get chatWindow() { + return this.env.models['mail.chat_window'].get(this.props.chatWindowLocalId); + } + + /** + * Get the content of placeholder for the autocomplete input of + * 'new_message' chat window. + * + * @returns {string} + */ + get newMessageFormInputPlaceholder() { + return this.env._t("Search user..."); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Apply visual position of the chat window. + * + * @private + */ + _applyVisibleOffset() { + const textDirection = this.env.messaging.locale.textDirection; + const offsetFrom = textDirection === 'rtl' ? 'left' : 'right'; + const oppositeFrom = offsetFrom === 'right' ? 'left' : 'right'; + this.el.style[offsetFrom] = this.chatWindow.visibleOffset + 'px'; + this.el.style[oppositeFrom] = 'auto'; + } + + /** + * Focus this chat window. + * + * @private + */ + _focus() { + this.chatWindow.update({ + isDoFocus: false, + isFocused: true, + }); + if (this._inputRef.comp) { + this._inputRef.comp.focus(); + } + if (this._threadRef.comp) { + this._threadRef.comp.focus(); + } + } + + /** + * Save the scroll positions of the chat window in the store. + * This is useful in order to remount chat windows and keep previous + * scroll positions. This is necessary because when toggling on/off + * home menu, the chat windows have to be remade from scratch. + * + * @private + */ + _saveThreadScrollTop() { + if ( + !this._threadRef.comp || + !this.chatWindow.threadViewer || + !this.chatWindow.threadViewer.threadView + ) { + return; + } + if (this.chatWindow.threadViewer.threadView.componentHintList.length > 0) { + // the current scroll position is likely incorrect due to the + // presence of hints to adjust it + return; + } + this.chatWindow.threadViewer.saveThreadCacheScrollHeightAsInitial( + this._threadRef.comp.getScrollHeight() + ); + this.chatWindow.threadViewer.saveThreadCacheScrollPositionsAsInitial( + this._threadRef.comp.getScrollTop() + ); + } + + /** + * @private + */ + _update() { + if (!this.chatWindow) { + // chat window is being deleted + return; + } + if (this.chatWindow.isDoFocus) { + this._focus(); + } + this._applyVisibleOffset(); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when selecting an item in the autocomplete input of the + * 'new_message' chat window. + * + * @private + * @param {Event} ev + * @param {Object} ui + * @param {Object} ui.item + * @param {integer} ui.item.id + */ + async _onAutocompleteSelect(ev, ui) { + const chat = await this.env.messaging.getChat({ partnerId: ui.item.id }); + if (!chat) { + return; + } + this.env.messaging.chatWindowManager.openThread(chat, { + makeActive: true, + replaceNewMessage: true, + }); + } + + /** + * Called when typing in the autocomplete input of the 'new_message' chat + * window. + * + * @private + * @param {Object} req + * @param {string} req.term + * @param {function} res + */ + _onAutocompleteSource(req, res) { + this.env.models['mail.partner'].imSearch({ + callback: (partners) => { + const suggestions = partners.map(partner => { + return { + id: partner.id, + value: partner.nameOrDisplayName, + label: partner.nameOrDisplayName, + }; + }); + res(_.sortBy(suggestions, 'label')); + }, + keyword: _.escape(req.term), + limit: 10, + }); + } + + /** + * Called when clicking on header of chat window. Usually folds the chat + * window. + * + * @private + * @param {CustomEvent} ev + */ + _onClickedHeader(ev) { + ev.stopPropagation(); + if (this.env.messaging.device.isMobile) { + return; + } + if (this.chatWindow.isFolded) { + this.chatWindow.unfold(); + this.chatWindow.focus(); + } else { + this._saveThreadScrollTop(); + this.chatWindow.fold(); + } + } + + /** + * Called when an element in the thread becomes focused. + * + * @private + * @param {FocusEvent} ev + */ + _onFocusinThread(ev) { + ev.stopPropagation(); + if (!this.chatWindow) { + // prevent crash on destroy + return; + } + this.chatWindow.update({ isFocused: true }); + } + + /** + * Focus out the chat window. + * + * @private + */ + _onFocusout() { + if (!this.chatWindow) { + // ignore focus out due to record being deleted + return; + } + this.chatWindow.update({ isFocused: false }); + } + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydown(ev) { + if (!this.chatWindow) { + // prevent crash during delete + return; + } + switch (ev.key) { + case 'Tab': + ev.preventDefault(); + if (ev.shiftKey) { + this.chatWindow.focusPreviousVisibleUnfoldedChatWindow(); + } else { + this.chatWindow.focusNextVisibleUnfoldedChatWindow(); + } + break; + case 'Escape': + if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) { + break; + } + if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + break; + } + ev.preventDefault(); + this.chatWindow.focusNextVisibleUnfoldedChatWindow(); + this.chatWindow.close(); + break; + } + } + + /** + * Save the scroll positions of the chat window in the store. + * This is useful in order to remount chat windows and keep previous + * scroll positions. This is necessary because when toggling on/off + * home menu, the chat windows have to be remade from scratch. + * + * @private + */ + async _onWillHideHomeMenu() { + this._saveThreadScrollTop(); + } + + /** + * Save the scroll positions of the chat window in the store. + * This is useful in order to remount chat windows and keep previous + * scroll positions. This is necessary because when toggling on/off + * home menu, the chat windows have to be remade from scratch. + * + * @private + */ + async _onWillShowHomeMenu() { + this._saveThreadScrollTop(); + } + +} + +Object.assign(ChatWindow, { + components, + defaultProps: { + hasCloseAsBackButton: false, + isExpandable: false, + isFullscreen: false, + }, + props: { + chatWindowLocalId: String, + hasCloseAsBackButton: Boolean, + isExpandable: Boolean, + isFullscreen: Boolean, + }, + template: 'mail.ChatWindow', +}); + +return patchMixin(ChatWindow); + +}); |
