diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/models/chat_window_manager | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/models/chat_window_manager')
| -rw-r--r-- | addons/mail/static/src/models/chat_window_manager/chat_window_manager.js | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js new file mode 100644 index 00000000..fc367fef --- /dev/null +++ b/addons/mail/static/src/models/chat_window_manager/chat_window_manager.js @@ -0,0 +1,487 @@ +odoo.define('mail/static/src/models/chat_window_manager/chat_window_manager.js', function (require) { +'use strict'; + +const { registerNewModel } = require('mail/static/src/model/model_core.js'); +const { attr, many2one, one2many, one2one } = require('mail/static/src/model/model_field.js'); + +function factory(dependencies) { + + const BASE_VISUAL = { + /** + * Amount of visible slots available for chat windows. + */ + availableVisibleSlots: 0, + /** + * Data related to the hidden menu. + */ + hidden: { + /** + * List of hidden docked chat windows. Useful to compute counter. + * Chat windows are ordered by their `chatWindows` order. + */ + chatWindowLocalIds: [], + /** + * Whether hidden menu is visible or not + */ + isVisible: false, + /** + * Offset of hidden menu starting point from the starting point + * of chat window manager. Makes only sense if it is visible. + */ + offset: 0, + }, + /** + * Data related to visible chat windows. Index determine order of + * docked chat windows. + * + * Value: + * + * { + * chatWindowLocalId, + * offset, + * } + * + * Offset is offset of starting point of docked chat window from + * starting point of dock chat window manager. Docked chat windows + * are ordered by their `chatWindows` order + */ + visible: [], + }; + + + class ChatWindowManager extends dependencies['mail.model'] { + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * Close all chat windows. + * + */ + closeAll() { + const chatWindows = this.allOrdered; + for (const chatWindow of chatWindows) { + chatWindow.close(); + } + } + + closeHiddenMenu() { + this.update({ isHiddenMenuOpen: false }); + } + + /** + * Closes all chat windows related to the given thread. + * + * @param {mail.thread} thread + * @param {Object} [options] + */ + closeThread(thread, options) { + for (const chatWindow of this.chatWindows) { + if (chatWindow.thread === thread) { + chatWindow.close(options); + } + } + } + + openHiddenMenu() { + this.update({ isHiddenMenuOpen: true }); + } + + openNewMessage() { + let newMessageChatWindow = this.newMessageChatWindow; + if (!newMessageChatWindow) { + newMessageChatWindow = this.env.models['mail.chat_window'].create({ + manager: [['link', this]], + }); + } + newMessageChatWindow.makeActive(); + } + + /** + * @param {mail.thread} thread + * @param {Object} [param1={}] + * @param {boolean} [param1.isFolded=false] + * @param {boolean} [param1.makeActive=false] + * @param {boolean} [param1.notifyServer] + * @param {boolean} [param1.replaceNewMessage=false] + */ + openThread(thread, { + isFolded = false, + makeActive = false, + notifyServer, + replaceNewMessage = false + } = {}) { + if (notifyServer === undefined) { + notifyServer = !this.env.messaging.device.isMobile; + } + let chatWindow = this.chatWindows.find(chatWindow => + chatWindow.thread === thread + ); + if (!chatWindow) { + chatWindow = this.env.models['mail.chat_window'].create({ + isFolded, + manager: [['link', this]], + thread: [['link', thread]], + }); + } else { + chatWindow.update({ isFolded }); + } + if (replaceNewMessage && this.newMessageChatWindow) { + this.swap(chatWindow, this.newMessageChatWindow); + this.newMessageChatWindow.close(); + } + if (makeActive) { + // avoid double notify at this step, it will already be done at + // the end of the current method + chatWindow.makeActive({ notifyServer: false }); + } + // Flux specific: notify server of chat window being opened. + if (notifyServer) { + const foldState = chatWindow.isFolded ? 'folded' : 'open'; + thread.notifyFoldStateToServer(foldState); + } + } + + /** + * Shift provided chat window to the left on screen. + * + * @param {mail.chat_window} chatWindow + */ + shiftLeft(chatWindow) { + const chatWindows = this.allOrdered; + const index = chatWindows.findIndex(cw => cw === chatWindow); + if (index === chatWindows.length - 1) { + // already left-most + return; + } + const otherChatWindow = chatWindows[index + 1]; + const _newOrdered = [...this._ordered]; + _newOrdered[index] = otherChatWindow.localId; + _newOrdered[index + 1] = chatWindow.localId; + this.update({ _ordered: _newOrdered }); + chatWindow.focus(); + } + + /** + * Shift provided chat window to the right on screen. + * + * @param {mail.chat_window} chatWindow + */ + shiftRight(chatWindow) { + const chatWindows = this.allOrdered; + const index = chatWindows.findIndex(cw => cw === chatWindow); + if (index === 0) { + // already right-most + return; + } + const otherChatWindow = chatWindows[index - 1]; + const _newOrdered = [...this._ordered]; + _newOrdered[index] = otherChatWindow.localId; + _newOrdered[index - 1] = chatWindow.localId; + this.update({ _ordered: _newOrdered }); + chatWindow.focus(); + } + + /** + * @param {mail.chat_window} chatWindow1 + * @param {mail.chat_window} chatWindow2 + */ + swap(chatWindow1, chatWindow2) { + const ordered = this.allOrdered; + const index1 = ordered.findIndex(chatWindow => chatWindow === chatWindow1); + const index2 = ordered.findIndex(chatWindow => chatWindow === chatWindow2); + if (index1 === -1 || index2 === -1) { + return; + } + const _newOrdered = [...this._ordered]; + _newOrdered[index1] = chatWindow2.localId; + _newOrdered[index2] = chatWindow1.localId; + this.update({ _ordered: _newOrdered }); + } + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @private + * @returns {string[]} + */ + _compute_ordered() { + // remove unlinked chatWindows + const _ordered = this._ordered.filter(chatWindowLocalId => + this.chatWindows.includes(this.env.models['mail.chat_window'].get(chatWindowLocalId)) + ); + // add linked chatWindows + for (const chatWindow of this.chatWindows) { + if (!_ordered.includes(chatWindow.localId)) { + _ordered.push(chatWindow.localId); + } + } + return _ordered; + } + + /** + * // FIXME: dependent on implementation that uses arbitrary order in relations!! + * + * @private + * @returns {mail.chat_window} + */ + _computeAllOrdered() { + return [['replace', this._ordered.map(chatWindowLocalId => + this.env.models['mail.chat_window'].get(chatWindowLocalId) + )]]; + } + + /** + * @private + * @returns {mail.chat_window[]} + */ + _computeAllOrderedHidden() { + return [['replace', this.visual.hidden.chatWindowLocalIds.map(chatWindowLocalId => + this.env.models['mail.chat_window'].get(chatWindowLocalId) + )]]; + } + + /** + * @private + * @returns {mail.chat_window[]} + */ + _computeAllOrderedVisible() { + return [['replace', this.visual.visible.map(({ chatWindowLocalId }) => + this.env.models['mail.chat_window'].get(chatWindowLocalId) + )]]; + } + + /** + * @private + * @returns {boolean} + */ + _computeHasHiddenChatWindows() { + return this.allOrderedHidden.length > 0; + } + + /** + * @private + * @returns {boolean} + */ + _computeHasVisibleChatWindows() { + return this.allOrderedVisible.length > 0; + } + + /** + * @private + * @returns {mail.chat_window|undefined} + */ + _computeLastVisible() { + const { length: l, [l - 1]: lastVisible } = this.allOrderedVisible; + if (!lastVisible) { + return [['unlink']]; + } + return [['link', lastVisible]]; + } + + /** + * @private + * @returns {mail.chat_window|undefined} + */ + _computeNewMessageChatWindow() { + const chatWindow = this.allOrdered.find(chatWindow => !chatWindow.thread); + if (!chatWindow) { + return [['unlink']]; + } + return [['link', chatWindow]]; + } + + /** + * @private + * @returns {integer} + */ + _computeUnreadHiddenConversationAmount() { + const allHiddenWithThread = this.allOrderedHidden.filter( + chatWindow => chatWindow.thread + ); + let amount = 0; + for (const chatWindow of allHiddenWithThread) { + if (chatWindow.thread.localMessageUnreadCounter > 0) { + amount++; + } + } + return amount; + } + + /** + * @private + * @returns {Object} + */ + _computeVisual() { + let visual = JSON.parse(JSON.stringify(BASE_VISUAL)); + if (!this.env.messaging) { + return visual; + } + const device = this.env.messaging.device; + const discuss = this.env.messaging.discuss; + const BETWEEN_GAP_WIDTH = 5; + const CHAT_WINDOW_WIDTH = 325; + const END_GAP_WIDTH = device.isMobile ? 0 : 10; + const GLOBAL_WINDOW_WIDTH = device.globalWindowInnerWidth; + const HIDDEN_MENU_WIDTH = 200; // max width, including width of dropup list items + const START_GAP_WIDTH = device.isMobile ? 0 : 10; + const chatWindows = this.allOrdered; + if (!device.isMobile && discuss.isOpen) { + return visual; + } + if (!chatWindows.length) { + return visual; + } + const relativeGlobalWindowWidth = GLOBAL_WINDOW_WIDTH - START_GAP_WIDTH - END_GAP_WIDTH; + let maxAmountWithoutHidden = Math.floor( + relativeGlobalWindowWidth / (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH)); + let maxAmountWithHidden = Math.floor( + (relativeGlobalWindowWidth - HIDDEN_MENU_WIDTH - BETWEEN_GAP_WIDTH) / + (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH)); + if (device.isMobile) { + maxAmountWithoutHidden = 1; + maxAmountWithHidden = 1; + } + if (chatWindows.length <= maxAmountWithoutHidden) { + // all visible + for (let i = 0; i < chatWindows.length; i++) { + const chatWindowLocalId = chatWindows[i].localId; + const offset = START_GAP_WIDTH + i * (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH); + visual.visible.push({ chatWindowLocalId, offset }); + } + visual.availableVisibleSlots = maxAmountWithoutHidden; + } else if (maxAmountWithHidden > 0) { + // some visible, some hidden + for (let i = 0; i < maxAmountWithHidden; i++) { + const chatWindowLocalId = chatWindows[i].localId; + const offset = START_GAP_WIDTH + i * (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH); + visual.visible.push({ chatWindowLocalId, offset }); + } + if (chatWindows.length > maxAmountWithHidden) { + visual.hidden.isVisible = !device.isMobile; + visual.hidden.offset = visual.visible[maxAmountWithHidden - 1].offset + + CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH; + } + for (let j = maxAmountWithHidden; j < chatWindows.length; j++) { + visual.hidden.chatWindowLocalIds.push(chatWindows[j].localId); + } + visual.availableVisibleSlots = maxAmountWithHidden; + } else { + // all hidden + visual.hidden.isVisible = !device.isMobile; + visual.hidden.offset = START_GAP_WIDTH; + visual.hidden.chatWindowLocalIds.concat(chatWindows.map(chatWindow => chatWindow.localId)); + console.warn('cannot display any visible chat windows (screen is too small)'); + visual.availableVisibleSlots = 0; + } + return visual; + } + + } + + ChatWindowManager.fields = { + /** + * List of ordered chat windows (list of local ids) + */ + _ordered: attr({ + compute: '_compute_ordered', + default: [], + dependencies: [ + 'chatWindows', + ], + }), + // FIXME: dependent on implementation that uses arbitrary order in relations!! + allOrdered: one2many('mail.chat_window', { + compute: '_computeAllOrdered', + dependencies: [ + '_ordered', + ], + }), + allOrderedThread: one2many('mail.thread', { + related: 'allOrdered.thread', + }), + allOrderedHidden: one2many('mail.chat_window', { + compute: '_computeAllOrderedHidden', + dependencies: ['visual'], + }), + allOrderedHiddenThread: one2many('mail.thread', { + related: 'allOrderedHidden.thread', + }), + allOrderedHiddenThreadMessageUnreadCounter: attr({ + related: 'allOrderedHiddenThread.localMessageUnreadCounter', + }), + allOrderedVisible: one2many('mail.chat_window', { + compute: '_computeAllOrderedVisible', + dependencies: ['visual'], + }), + chatWindows: one2many('mail.chat_window', { + inverse: 'manager', + isCausal: true, + }), + device: one2one('mail.device', { + related: 'messaging.device', + }), + deviceGlobalWindowInnerWidth: attr({ + related: 'device.globalWindowInnerWidth', + }), + deviceIsMobile: attr({ + related: 'device.isMobile', + }), + discuss: one2one('mail.discuss', { + related: 'messaging.discuss', + }), + discussIsOpen: attr({ + related: 'discuss.isOpen', + }), + hasHiddenChatWindows: attr({ + compute: '_computeHasHiddenChatWindows', + dependencies: ['allOrderedHidden'], + }), + hasVisibleChatWindows: attr({ + compute: '_computeHasVisibleChatWindows', + dependencies: ['allOrderedVisible'], + }), + isHiddenMenuOpen: attr({ + default: false, + }), + lastVisible: many2one('mail.chat_window', { + compute: '_computeLastVisible', + dependencies: ['allOrderedVisible'], + }), + messaging: one2one('mail.messaging', { + inverse: 'chatWindowManager', + }), + newMessageChatWindow: one2one('mail.chat_window', { + compute: '_computeNewMessageChatWindow', + dependencies: [ + 'allOrdered', + 'allOrderedThread', + ], + }), + unreadHiddenConversationAmount: attr({ + compute: '_computeUnreadHiddenConversationAmount', + dependencies: ['allOrderedHiddenThreadMessageUnreadCounter'], + }), + visual: attr({ + compute: '_computeVisual', + default: BASE_VISUAL, + dependencies: [ + 'allOrdered', + 'deviceGlobalWindowInnerWidth', + 'deviceIsMobile', + 'discussIsOpen', + ], + }), + }; + + ChatWindowManager.modelName = 'mail.chat_window_manager'; + + return ChatWindowManager; +} + +registerNewModel('mail.chat_window_manager', factory); + +}); |
