summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/models/chat_window_manager
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/models/chat_window_manager
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js487
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);
+
+});