diff --git a/addons/mail/static/src/components/call_view/call_view.js b/addons/mail/static/src/components/call_view/call_view.js index cffaf2c6ca7..c1ce1fea89f 100644 --- a/addons/mail/static/src/components/call_view/call_view.js +++ b/addons/mail/static/src/components/call_view/call_view.js @@ -1,11 +1,12 @@ /** @odoo-module **/ +import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; import { useUpdate } from '@mail/component_hooks/use_update'; import { registerMessagingComponent } from '@mail/utils/messaging_component'; import Dialog from 'web.OwlDialog'; -const { Component, useRef, useState } = owl; +const { Component } = owl; const components = { Dialog, @@ -20,12 +21,7 @@ export class CallView extends Component { */ setup() { super.setup(); - this.state = useState({ - tileWidth: 0, - tileHeight: 0, - columnCount: 0, - }); - this.tileContainerRef = useRef('tileContainer'); + useRefToModel({ fieldName: 'tileContainerRef', refName: 'tileContainer', }); useUpdate({ func: () => this._update() }); } @@ -44,52 +40,6 @@ export class CallView extends Component { // Private //-------------------------------------------------------------------------- - /** - * Finds a tile layout and dimensions that respects param0.aspectRatio while maximizing - * the total area covered by the tiles within the specified container dimensions. - * - * @private - * @param {Object} param0 - * @param {number} [param0.aspectRatio] - * @param {number} param0.containerHeight - * @param {number} param0.containerWidth - * @param {number} param0.tileCount - */ - _computeTessellation({ aspectRatio = 1, containerHeight, containerWidth, tileCount }) { - let optimalLayout = { - area: 0, - cols: 0, - tileHeight: 0, - tileWidth: 0, - }; - - for (let columnCount = 1; columnCount <= tileCount; columnCount++) { - const rowCount = Math.ceil(tileCount / columnCount); - const potentialHeight = containerWidth / (columnCount * aspectRatio); - const potentialWidth = containerHeight / rowCount; - let tileHeight; - let tileWidth; - if (potentialHeight > potentialWidth) { - tileHeight = Math.floor(potentialWidth); - tileWidth = Math.floor(tileHeight * aspectRatio); - } else { - tileWidth = Math.floor(containerWidth / columnCount); - tileHeight = Math.floor(tileWidth / aspectRatio); - } - const area = tileHeight * tileWidth; - if (area <= optimalLayout.area) { - continue; - } - optimalLayout = { - area, - columnCount, - tileHeight, - tileWidth, - }; - } - return optimalLayout; - } - /** * @private */ @@ -97,21 +47,22 @@ export class CallView extends Component { if (!this.root.el) { return; } - if (!this.tileContainerRef.el) { + if (!this.callView.tileContainerRef.el) { return; } - const { width, height } = this.tileContainerRef.el.getBoundingClientRect(); + const { width, height } = this.callView.tileContainerRef.el.getBoundingClientRect(); - const { tileWidth, tileHeight, columnCount } = this._computeTessellation({ + const { tileWidth, tileHeight } = this.callView.calculateTessellation({ aspectRatio: this.callView.aspectRatio, containerHeight: height, containerWidth: width, - tileCount: this.tileContainerRef.el.children.length, + tileCount: this.callView.tileContainerRef.el.children.length, }); - this.state.tileWidth = tileWidth; - this.state.tileHeight = tileHeight; - this.state.columnCount = columnCount; + this.callView.update({ + tileHeight, + tileWidth, + }); } /** diff --git a/addons/mail/static/src/components/call_view/call_view.xml b/addons/mail/static/src/components/call_view/call_view.xml index 5e2e37736b3..935f1268b6e 100644 --- a/addons/mail/static/src/components/call_view/call_view.xml +++ b/addons/mail/static/src/components/call_view/call_view.xml @@ -27,7 +27,7 @@ 'o-isSidebar': callView.hasSidebar and callView.mainParticipantCard, }" t-ref="tileContainer" - t-attf-style="--height:{{state.tileHeight}}px; --width:{{state.tileWidth}}px; --columns:{{state.columnCount}};" + t-attf-style="--height:{{state.tileHeight}}px; --width:{{state.tileWidth}}px;" > diff --git a/addons/mail/static/src/components/channel_member/channel_member.js b/addons/mail/static/src/components/channel_member/channel_member.js index 20067057030..91a6d439def 100644 --- a/addons/mail/static/src/components/channel_member/channel_member.js +++ b/addons/mail/static/src/components/channel_member/channel_member.js @@ -14,10 +14,10 @@ export class ChannelMember extends Component { } /** - * @returns {Partner} + * @returns {ChannelMemberView} */ - get member() { - return this.props.member; + get record() { + return this.props.record; } } @@ -25,7 +25,7 @@ export class ChannelMember extends Component { Object.assign(ChannelMember, { props: { channel: Object, - member: Object, + record: Object, }, template: 'mail.ChannelMember', }); diff --git a/addons/mail/static/src/components/channel_member/channel_member.xml b/addons/mail/static/src/components/channel_member/channel_member.xml index 4f1f7ca3130..657661de698 100644 --- a/addons/mail/static/src/components/channel_member/channel_member.xml +++ b/addons/mail/static/src/components/channel_member/channel_member.xml @@ -2,23 +2,23 @@ -
-
- Avatar +
+
+ Avatar - +
- - + +
diff --git a/addons/mail/static/src/components/channel_member_list/channel_member_list.xml b/addons/mail/static/src/components/channel_member_list/channel_member_list.xml index 1cb477dd8c2..9166b7d197f 100644 --- a/addons/mail/static/src/components/channel_member_list/channel_member_list.xml +++ b/addons/mail/static/src/components/channel_member_list/channel_member_list.xml @@ -5,10 +5,10 @@
- + - + And 1 other member. diff --git a/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.js b/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.js index 2c3704289e3..60878121b4b 100644 --- a/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.js +++ b/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.js @@ -2,19 +2,10 @@ import { registerMessagingComponent } from '@mail/utils/messaging_component'; -import { sprintf } from '@web/core/utils/strings'; - const { Component } = owl; export class ChannelMemberListCategory extends Component { - /** - * @returns {ChannelMemberListView} - */ - get channelMemberListView() { - return this.props.channelMemberListView; - } - /** * @returns {ChannelMemberListCategoryView} */ @@ -22,49 +13,10 @@ export class ChannelMemberListCategory extends Component { return this.props.record; } - /** - * @returns {Partner[]} - */ - get members() { - if (!this.channelMemberListView.exists()) { - return []; - } - if (this.record.channelMemberListViewOwnerAsOnline) { - return this.channelMemberListView.channel.orderedOnlineMembers; - } - if (this.record.channelMemberListViewOwnerAsOffline) { - return this.channelMemberListView.channel.orderedOfflineMembers; - } - return []; - } - - /** - * @returns {string} - */ - get title() { - let categoryText = ""; - if (this.record.channelMemberListViewOwnerAsOnline) { - categoryText = this.env._t("Online"); - } - if (this.record.channelMemberListViewOwnerAsOffline) { - categoryText = this.env._t("Offline"); - } - return sprintf( - this.env._t("%(categoryText)s - %(memberCount)s"), - { - categoryText, - memberCount: this.members.length, - } - ); - } - } Object.assign(ChannelMemberListCategory, { - props: { - channelMemberListView: Object, - record: Object, - }, + props: { record: Object }, template: 'mail.ChannelMemberListCategory', }); diff --git a/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.xml b/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.xml index be4128e7837..c60c1fb0ba9 100644 --- a/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.xml +++ b/addons/mail/static/src/components/channel_member_list_category/channel_member_list_category.xml @@ -2,9 +2,9 @@ -
- - +
+ + diff --git a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js index 6c57455dd54..4bfafda14f7 100644 --- a/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js +++ b/addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js @@ -72,7 +72,7 @@ export class ChatWindowHiddenMenu extends Component { const textDirection = this.messaging.locale.textDirection; const offsetFrom = textDirection === 'rtl' ? 'left' : 'right'; const oppositeFrom = offsetFrom === 'right' ? 'left' : 'right'; - const offset = this.messaging.chatWindowManager.visual.hidden.offset; + const offset = this.messaging.chatWindowManager.visual.hiddenMenuOffset; this.root.el.style[offsetFrom] = `${offset}px`; this.root.el.style[oppositeFrom] = 'auto'; } diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml index e733d7b8897..bfa979334e4 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml @@ -4,7 +4,7 @@
- + diff --git a/addons/mail/static/src/models/call_view.js b/addons/mail/static/src/models/call_view.js index d836dbbb315..b39745ddecf 100644 --- a/addons/mail/static/src/models/call_view.js +++ b/addons/mail/static/src/models/call_view.js @@ -17,11 +17,53 @@ registerModel({ browser.addEventListener('fullscreenchange', this._onFullScreenChange); }, _willDelete() { - browser.clearTimeout(this.showOverlayTimeout); browser.removeEventListener('fullscreenchange', this._onFullScreenChange); }, }, recordMethods: { + /** + * Finds a tile layout and dimensions that respects param0.aspectRatio while maximizing + * the total area covered by the tiles within the specified container dimensions. + * + * @param {Object} param0 + * @param {number} [param0.aspectRatio] + * @param {number} param0.containerHeight + * @param {number} param0.containerWidth + * @param {number} param0.tileCount + */ + calculateTessellation({ aspectRatio = 1, containerHeight, containerWidth, tileCount }) { + let optimalLayout = { + area: 0, + cols: 0, + tileHeight: 0, + tileWidth: 0, + }; + + for (let columnCount = 1; columnCount <= tileCount; columnCount++) { + const rowCount = Math.ceil(tileCount / columnCount); + const potentialHeight = containerWidth / (columnCount * aspectRatio); + const potentialWidth = containerHeight / rowCount; + let tileHeight; + let tileWidth; + if (potentialHeight > potentialWidth) { + tileHeight = Math.floor(potentialWidth); + tileWidth = Math.floor(tileHeight * aspectRatio); + } else { + tileWidth = Math.floor(containerWidth / columnCount); + tileHeight = Math.floor(tileWidth / aspectRatio); + } + const area = tileHeight * tileWidth; + if (area <= optimalLayout.area) { + continue; + } + optimalLayout = { + area, + tileHeight, + tileWidth, + }; + } + return optimalLayout; + }, /** * @param {MouseEvent} ev */ @@ -75,8 +117,8 @@ registerModel({ markEventHandled(ev, 'CallView.MouseMoveOverlay'); this.update({ showOverlay: true, + showOverlayTimer: clear(), }); - browser.clearTimeout(this.showOverlayTimeout); }, /** * @param {MouseEvent} ev @@ -84,6 +126,9 @@ registerModel({ onRtcSettingsDialogClosed(ev) { this.messaging.userSetting.callSettingsMenu.toggle(); }, + onShowOverlayTimeout() { + this.update({ showOverlay: false }); + }, async activateFullScreen() { const el = document.body; try { @@ -249,10 +294,7 @@ registerModel({ _showOverlay() { this.update({ showOverlay: true, - }); - browser.clearTimeout(this.showOverlayTimeout); - this.update({ - showOverlayTimeout: browser.setTimeout(this._onShowOverlayTimeout, 3000), + showOverlayTimer: [clear(), insertAndReplace()], }); }, /** @@ -266,15 +308,6 @@ registerModel({ } this.update({ isFullScreen: false }); }, - /** - * @private - */ - _onShowOverlayTimeout() { - if (!this.exists()) { - return; - } - this.update({ showOverlay: false }); - }, }, fields: { /** @@ -288,6 +321,7 @@ registerModel({ default: 16 / 9, compute: '_computeAspectRatio', }), + tileContainerRef: attr(), /** * Determines whether we only display the videos or all the participants */ @@ -364,7 +398,10 @@ registerModel({ showOverlay: attr({ default: true, }), - showOverlayTimeout: attr(), + showOverlayTimer: one('Timer', { + inverse: 'callViewAsShowOverlay', + isCausal: true, + }), /** * ThreadView on which the call view is attached. */ @@ -373,6 +410,9 @@ registerModel({ readonly: true, required: true, }), + tileHeight: attr({ + default: 0, + }), /** * List of all participant cards (can either be invitations or rtcSessions). */ @@ -381,6 +421,9 @@ registerModel({ inverse: 'callViewOfTile', isCausal: true, }), + tileWidth: attr({ + default: 0, + }), }, onChanges: [ new OnChange({ diff --git a/addons/mail/static/src/models/channel_member_list_category_view.js b/addons/mail/static/src/models/channel_member_list_category_view.js index 93d2bb42860..47a82e0021e 100644 --- a/addons/mail/static/src/models/channel_member_list_category_view.js +++ b/addons/mail/static/src/models/channel_member_list_category_view.js @@ -1,12 +1,81 @@ /** @odoo-module **/ import { registerModel } from '@mail/model/model_core'; -import { one } from '@mail/model/model_field'; +import { attr, many, one } from '@mail/model/model_field'; +import { clear, insertAndReplace, replace } from '@mail/model/model_field_command'; + +import { sprintf } from '@web/core/utils/strings'; registerModel({ name: 'ChannelMemberListCategoryView', identifyingFields: [['channelMemberListViewOwnerAsOffline', 'channelMemberListViewOwnerAsOnline']], + recordMethods: { + /** + * @private + * @returns {FieldCommand} + */ + _computeChannel() { + if (this.channelMemberListViewOwnerAsOffline) { + return replace(this.channelMemberListViewOwnerAsOffline.channel); + } + if (this.channelMemberListViewOwnerAsOnline) { + return replace(this.channelMemberListViewOwnerAsOnline.channel); + } + }, + /** + * @private + * @returns {FieldCommand} + */ + _computeChannelMemberViews() { + if (this.members.length === 0) { + return clear(); + } + return insertAndReplace( + this.members.map(member => ({ partner: replace(member) })), + ); + }, + /** + * @private + * @returns {FieldCommand} + */ + _computeMembers() { + if (!this.exists()) { + return clear(); + } + if (this.channelMemberListViewOwnerAsOnline) { + return replace(this.channel.orderedOnlineMembers); + } + if (this.channelMemberListViewOwnerAsOffline) { + return replace(this.channel.orderedOfflineMembers); + } + return clear(); + }, + /** + * @private + * @returns {string} + */ + _computeTitle() { + let categoryText = ""; + if (this.channelMemberListViewOwnerAsOnline) { + categoryText = this.env._t("Online"); + } + if (this.channelMemberListViewOwnerAsOffline) { + categoryText = this.env._t("Offline"); + } + return sprintf( + this.env._t("%(categoryText)s - %(memberCount)s"), + { + categoryText, + memberCount: this.members.length, + } + ); + }, + }, fields: { + channel: one('Thread', { + compute: '_computeChannel', + required: true, + }), channelMemberListViewOwnerAsOffline: one('ChannelMemberListView', { inverse: 'offlineCategoryView', readonly: true, @@ -15,5 +84,16 @@ registerModel({ inverse: 'onlineCategoryView', readonly: true, }), + channelMemberViews: many('ChannelMemberView', { + compute: '_computeChannelMemberViews', + inverse: 'channelMemberListCategoryViewOwner', + isCausal: true, + }), + members: many('Partner', { + compute: '_computeMembers', + }), + title: attr({ + compute: '_computeTitle', + }), }, }); diff --git a/addons/mail/static/src/models/channel_member_view.js b/addons/mail/static/src/models/channel_member_view.js new file mode 100644 index 00000000000..5e1e1909445 --- /dev/null +++ b/addons/mail/static/src/models/channel_member_view.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import { registerModel } from '@mail/model/model_core'; +import { one } from '@mail/model/model_field'; + +registerModel({ + name: 'ChannelMemberView', + identifyingFields: ['channelMemberListCategoryViewOwner', 'partner'], + fields: { + channelMemberListCategoryViewOwner: one('ChannelMemberListCategoryView', { + inverse: 'channelMemberViews', + readonly: true, + required: true, + }), + partner: one('Partner', { + inverse: 'channelMemberViews', + readonly: true, + required: true, + }), + }, +}); diff --git a/addons/mail/static/src/models/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager.js index 2c9fc59d709..e9331ec530a 100644 --- a/addons/mail/static/src/models/chat_window_manager.js +++ b/addons/mail/static/src/models/chat_window_manager.js @@ -6,28 +6,24 @@ import { clear, insertAndReplace, link, replace } from '@mail/model/model_field_ const BASE_VISUAL = { /** + * @deprecated, should use ChatWindowManager/availableVisibleSlots instead * Amount of visible slots available for chat windows. */ availableVisibleSlots: 0, /** - * Data related to the hidden menu. + * List of hidden docked chat windows. Useful to compute counter. + * Chat windows are ordered by their `chatWindows` order. */ - 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, - }, + hiddenChatWindowLocalIds: [], + /** + * Whether hidden menu is visible or not + */ + isHiddenMenuVisible: false, + /** + * Offset of hidden menu starting point from the starting point + * of chat window manager. Makes only sense if it is visible. + */ + hiddenMenuOffset: 0, /** * Data related to visible chat windows. Index determine order of * docked chat windows. @@ -223,7 +219,7 @@ registerModel({ * @returns {ChatWindow[]} */ _computeAllOrderedHidden() { - return replace(this.visual.hidden.chatWindowLocalIds.map(chatWindowLocalId => + return replace(this.visual.hiddenChatWindowLocalIds.map(chatWindowLocalId => this.messaging.models['ChatWindow'].get(chatWindowLocalId) )); }, @@ -236,6 +232,23 @@ registerModel({ this.messaging.models['ChatWindow'].get(chatWindowLocalId) )); }, + /** + * @private + * @returns {integer} + */ + _computeAvailableVisibleSlots() { + return this.visual.availableVisibleSlots; + }, + /** + * @private + * @returns {integer} + */ + _computeEndGapWidth() { + if (this.messaging.device.isSmall) { + return 0; + } + return 10; + }, /** * @private * @returns {boolean} @@ -264,6 +277,16 @@ registerModel({ } return replace(lastVisible); }, + /** + * @private + * @returns {integer} + */ + _computeStartGapWidth() { + if (this.messaging.device.isSmall) { + return 0; + } + return 10; + }, /** * @private * @returns {integer} @@ -289,23 +312,18 @@ registerModel({ if (!this.messaging || !this.messaging.device) { return visual; } - const BETWEEN_GAP_WIDTH = 5; - const CHAT_WINDOW_WIDTH = 325; - const END_GAP_WIDTH = this.messaging.device.isSmall ? 0 : 10; - const HIDDEN_MENU_WIDTH = 200; // max width, including width of dropup list items - const START_GAP_WIDTH = this.messaging.device.isSmall ? 0 : 10; if (!this.messaging.device.isSmall && this.messaging.discuss.discussView) { return visual; } if (!this.allOrdered.length) { return visual; } - const relativeGlobalWindowWidth = this.messaging.device.globalWindowInnerWidth - START_GAP_WIDTH - END_GAP_WIDTH; + const relativeGlobalWindowWidth = this.messaging.device.globalWindowInnerWidth - this.startGapWidth - this.endGapWidth; let maxAmountWithoutHidden = Math.floor( - relativeGlobalWindowWidth / (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH)); + relativeGlobalWindowWidth / (this.chatWindowWidth + this.betweenGapWidth)); let maxAmountWithHidden = Math.floor( - (relativeGlobalWindowWidth - HIDDEN_MENU_WIDTH - BETWEEN_GAP_WIDTH) / - (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH)); + (relativeGlobalWindowWidth - this.hiddenMenuWidth - this.betweenGapWidth) / + (this.chatWindowWidth + this.betweenGapWidth)); if (this.messaging.device.isSmall) { maxAmountWithoutHidden = 1; maxAmountWithHidden = 1; @@ -314,7 +332,7 @@ registerModel({ // all visible for (let i = 0; i < this.allOrdered.length; i++) { const chatWindowLocalId = this.allOrdered[i].localId; - const offset = START_GAP_WIDTH + i * (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH); + const offset = this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); visual.visible.push({ chatWindowLocalId, offset }); } visual.availableVisibleSlots = maxAmountWithoutHidden; @@ -322,23 +340,23 @@ registerModel({ // some visible, some hidden for (let i = 0; i < maxAmountWithHidden; i++) { const chatWindowLocalId = this.allOrdered[i].localId; - const offset = START_GAP_WIDTH + i * (CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH); + const offset = this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); visual.visible.push({ chatWindowLocalId, offset }); } if (this.allOrdered.length > maxAmountWithHidden) { - visual.hidden.isVisible = !this.messaging.device.isSmall; - visual.hidden.offset = visual.visible[maxAmountWithHidden - 1].offset - + CHAT_WINDOW_WIDTH + BETWEEN_GAP_WIDTH; + visual.isHiddenMenuVisible = !this.messaging.device.isSmall; + visual.hiddenMenuOffset = visual.visible[maxAmountWithHidden - 1].offset + + this.chatWindowWidth + this.betweenGapWidth; } for (let j = maxAmountWithHidden; j < this.allOrdered.length; j++) { - visual.hidden.chatWindowLocalIds.push(this.allOrdered[j].localId); + visual.hiddenChatWindowLocalIds.push(this.allOrdered[j].localId); } visual.availableVisibleSlots = maxAmountWithHidden; } else { // all hidden - visual.hidden.isVisible = !this.messaging.device.isSmall; - visual.hidden.offset = START_GAP_WIDTH; - visual.hidden.chatWindowLocalIds.concat(this.allOrdered.map(chatWindow => chatWindow.localId)); + visual.isHiddenMenuVisible = !this.messaging.device.isSmall; + visual.hiddenMenuOffset = this.startGapWidth; + visual.hiddenChatWindowLocalIds.concat(this.allOrdered.map(chatWindow => chatWindow.localId)); console.warn('cannot display any visible chat windows (screen is too small)'); visual.availableVisibleSlots = 0; } @@ -358,16 +376,35 @@ registerModel({ allOrderedVisible: many('ChatWindow', { compute: '_computeAllOrderedVisible', }), + /** + * Amount of visible slots available for chat windows. + */ + availableVisibleSlots: attr({ + compute: '_computeAvailableVisibleSlots', + default: 0, + }), + betweenGapWidth: attr({ + default: 5, + }), chatWindows: many('ChatWindow', { inverse: 'manager', isCausal: true, }), + chatWindowWidth: attr({ + default: 325, + }), + endGapWidth: attr({ + compute: '_computeEndGapWidth', + }), hasVisibleChatWindows: attr({ compute: '_computeHasVisibleChatWindows', }), hiddenChatWindowHeaderViews: many('ChatWindowHeaderView', { compute: '_computeHiddenChatWindowHeaderViews', }), + hiddenMenuWidth: attr({ + default: 200, // max width, including width of dropup list items + }), isHiddenMenuOpen: attr({ default: false, }), @@ -378,6 +415,9 @@ registerModel({ inverse: 'managerAsNewMessage', isCausal: true, }), + startGapWidth: attr({ + compute: '_computeStartGapWidth', + }), unreadHiddenConversationAmount: attr({ compute: '_computeUnreadHiddenConversationAmount', }), diff --git a/addons/mail/static/src/models/chatter.js b/addons/mail/static/src/models/chatter.js index 115f2be1ba4..75e2d28d1a7 100644 --- a/addons/mail/static/src/models/chatter.js +++ b/addons/mail/static/src/models/chatter.js @@ -24,17 +24,15 @@ const getMessageNextTemporaryId = (function () { registerModel({ name: 'Chatter', identifyingFields: ['id'], - lifecycleHooks: { - _willDelete() { - this.messaging.browser.clearTimeout(this.attachmentsLoaderTimeout); - }, - }, recordMethods: { focus() { if (this.composerView) { this.composerView.update({ doFocus: true }); } }, + onAttachmentsLoadingTimeout() { + this.update({ isShowingAttachmentsLoading: true }); + }, /** * Handles click on the attachments button. * @@ -246,7 +244,7 @@ registerModel({ * @returns {boolean} */ _computeIsPreparingAttachmentsLoading() { - return this.attachmentsLoaderTimeout !== undefined; + return Boolean(this.attachmentsLoaderTimer); }, /** * @private @@ -259,15 +257,6 @@ registerModel({ thread: this.thread ? replace(this.thread) : clear(), }); }, - /** - * @private - */ - _onAttachmentsLoadingTimeout() { - this.update({ - attachmentsLoaderTimeout: clear(), - isShowingAttachmentsLoading: true, - }); - }, /** * @private */ @@ -325,19 +314,13 @@ registerModel({ * @private */ _prepareAttachmentsLoading() { - this.update({ - attachmentsLoaderTimeout: this.messaging.browser.setTimeout( - this._onAttachmentsLoadingTimeout, - this.messaging.loadingBaseDelayDuration, - ), - }); + this.update({ attachmentsLoaderTimer: insertAndReplace() }); }, /** * @private */ _stopAttachmentsLoading() { - this.messaging.browser.clearTimeout(this.attachmentsLoaderTimeout); - this.update({ attachmentsLoaderTimeout: clear() }); + this.update({ attachmentsLoaderTimer: clear() }); }, }, fields: { @@ -350,7 +333,10 @@ registerModel({ inverse: 'chatter', isCausal: true, }), - attachmentsLoaderTimeout: attr(), + attachmentsLoaderTimer: one('Timer', { + inverse: 'chatterOwnerAsAttachmentsLoader', + isCausal: true, + }), /** * States the OWL Chatter component of this chatter. */ diff --git a/addons/mail/static/src/models/media_preview.js b/addons/mail/static/src/models/media_preview.js index a999fe84cff..58e53ee94b9 100644 --- a/addons/mail/static/src/models/media_preview.js +++ b/addons/mail/static/src/models/media_preview.js @@ -6,19 +6,6 @@ import { registerModel } from '@mail/model/model_core'; registerModel({ name: 'MediaPreview', identifyingFields: ['welcomeView'], - modelMethods: { - /** - * Iterates tracks of the provided MediaStream, calling the `stop` - * method on each of them. - * - * @param {MediaStream} mediaStream - */ - stopTracksOnMediaStream(mediaStream) { - for (const track of mediaStream.getTracks()) { - track.stop(); - } - }, - }, recordMethods: { /** * Stops recording user's microphone. @@ -28,7 +15,7 @@ registerModel({ if (!this.audioStream) { return; } - this.messaging.models['MediaPreview'].stopTracksOnMediaStream(this.audioStream); + this.stopTracksOnMediaStream(this.audioStream); this.update({ audioStream: null }); }, /** @@ -39,7 +26,7 @@ registerModel({ if (!this.videoStream) { return; } - this.messaging.models['MediaPreview'].stopTracksOnMediaStream(this.videoStream); + this.stopTracksOnMediaStream(this.videoStream); this.update({ videoStream: null }); }, /** @@ -100,6 +87,17 @@ registerModel({ onClickEnableVideoButton() { this.enableVideo(); }, + /** + * Iterates tracks of the provided MediaStream, calling the `stop` + * method on each of them. + * + * @param {MediaStream} mediaStream + */ + stopTracksOnMediaStream(mediaStream) { + for (const track of mediaStream.getTracks()) { + track.stop(); + } + }, /** * @private * @returns {boolean} diff --git a/addons/mail/static/src/models/message_view.js b/addons/mail/static/src/models/message_view.js index 90f00ae2ac5..29848229605 100644 --- a/addons/mail/static/src/models/message_view.js +++ b/addons/mail/static/src/models/message_view.js @@ -13,15 +13,9 @@ registerModel({ * Briefly highlights the message. */ highlight() { - this.messaging.browser.clearTimeout(this.highlightTimeout); this.update({ + highlightTimer: [clear(), insertAndReplace()], isHighlighted: true, - highlightTimeout: this.messaging.browser.setTimeout(() => { - if (!this.exists()) { - return; - } - this.update({ isHighlighted: false }); - }, 2000), }); }, /** @@ -110,6 +104,9 @@ registerModel({ this.threadView.handleVisibleMessage(this.message); } }, + onHighlightTimerTimeout() { + this.update({ isHighlighted: false }); + }, onMouseenter() { this.update({ isHovered: true }); }, @@ -368,9 +365,12 @@ registerModel({ compute: '_computeHasAuthorOpenChat', }), /** - * id of the current timeout that will reset isHighlighted to false. + * Current timer that will reset isHighlighted to false. */ - highlightTimeout: attr(), + highlightTimer: one('Timer', { + inverse: 'messageViewOwnerAsHighlight', + isCausal: true, + }), /** * Whether the message is "active", ie: hovered or clicked, and should * display additional things (date in sidebar, message actions, etc.) diff --git a/addons/mail/static/src/models/partner.js b/addons/mail/static/src/models/partner.js index b213e871141..1e7bb3949c8 100644 --- a/addons/mail/static/src/models/partner.js +++ b/addons/mail/static/src/models/partner.js @@ -376,6 +376,10 @@ registerModel({ inverse: 'partner', isCausal: true, }), + channelMemberViews: many('ChannelMemberView', { + inverse: 'partner', + isCausal: true, + }), country: one('Country'), /** * Deprecated. diff --git a/addons/mail/static/src/models/rtc_session.js b/addons/mail/static/src/models/rtc_session.js index 2a613ccdda6..36b24c836a4 100644 --- a/addons/mail/static/src/models/rtc_session.js +++ b/addons/mail/static/src/models/rtc_session.js @@ -2,7 +2,7 @@ import { registerModel } from '@mail/model/model_core'; import { attr, many, one } from '@mail/model/model_field'; -import { clear, replace } from '@mail/model/model_field_command'; +import { clear, insertAndReplace, replace } from '@mail/model/model_field_command'; registerModel({ name: 'RtcSession', @@ -13,17 +13,33 @@ registerModel({ }, }, recordMethods: { + onBroadcastTimeout() { + this.messaging.rpc( + { + route: '/mail/rtc/session/update_and_broadcast', + params: { + session_id: this.id, + values: { + is_camera_on: this.isCameraOn, + is_deaf: this.isDeaf, + is_muted: this.isSelfMuted, + is_screen_sharing_on: this.isScreenSharingOn, + }, + }, + }, + { shadow: true }, + ); + }, /** * restores the session to its default values */ reset() { - this.messaging.browser.clearTimeout(this.broadcastTimeout); this.messaging.browser.clearTimeout(this.connectionRecoveryTimeout); this._removeAudio(); this.removeVideo(); this.update({ audioElement: clear(), - broadcastTimeout: clear(), + broadcastTimer: clear(), connectionRecoveryTimeout: clear(), isTalking: clear(), localCandidateType: clear(), @@ -96,11 +112,8 @@ registerModel({ if (!this.rtcAsCurrentSession) { return; } - this.update(data); - this.messaging.browser.clearTimeout(this.broadcastTimeout); - this.update({ - broadcastTimeout: this.messaging.browser.setTimeout(this._onBroadcastTimeout, 3000), - }); + const data2 = Object.assign({}, data, { broadcastTimer: [clear(), insertAndReplace()] }); + this.update(data2); }, /** * Updates the rtcSession with information on the type of candidate used @@ -280,30 +293,6 @@ registerModel({ return this.audioElement.volume; } }, - /** - * @private - */ - _onBroadcastTimeout() { - if (!this.exists()) { - return; - } - this.update({ broadcastTimeout: clear() }); - this.messaging.rpc( - { - route: '/mail/rtc/session/update_and_broadcast', - params: { - session_id: this.id, - values: { - is_camera_on: this.isCameraOn, - is_deaf: this.isDeaf, - is_muted: this.isSelfMuted, - is_screen_sharing_on: this.isScreenSharingOn, - }, - }, - }, - { shadow: true }, - ); - }, /** * cleanly removes the audio stream of the session * @@ -396,7 +385,10 @@ registerModel({ avatarUrl: attr({ compute: '_computeAvatarUrl', }), - broadcastTimeout: attr(), + broadcastTimer: one('Timer', { + inverse: 'rtcSessionOwnerAsBroadcast', + isCausal: true, + }), /** * The mail.channel of the session, rtc sessions are part and managed by * mail.channel diff --git a/addons/mail/static/src/models/timer.js b/addons/mail/static/src/models/timer.js index 94789b65a1c..3b9813be4e8 100644 --- a/addons/mail/static/src/models/timer.js +++ b/addons/mail/static/src/models/timer.js @@ -7,8 +7,12 @@ import { clear } from '@mail/model/model_field_command'; registerModel({ name: 'Timer', identifyingFields: [[ + 'callViewAsShowOverlay', + 'chatterOwnerAsAttachmentsLoader', 'messagingOwnerAsFetchImStatusTimer', + 'messageViewOwnerAsHighlight', 'otherMemberLongTypingInThreadTimerOwner', + 'rtcSessionOwnerAsBroadcast', 'threadAsCurrentPartnerInactiveTypingTimerOwner', 'threadAsCurrentPartnerLongTypingTimerOwner', 'throttleOwner', @@ -24,9 +28,21 @@ registerModel({ * @returns {integer|FieldCommand} */ _computeDuration() { + if (this.callViewAsShowOverlay) { + return 3 * 1000; + } + if (this.chatterOwnerAsAttachmentsLoader) { + return this.messaging.loadingBaseDelayDuration; + } + if (this.messageViewOwnerAsHighlight) { + return 2 * 1000; + } if (this.messagingOwnerAsFetchImStatusTimer) { return this.messagingOwnerAsFetchImStatusTimer.fetchImStatusTimerDuration; } + if (this.rtcSessionOwnerAsBroadcast) { + return 3 * 1000; + } if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) { return 5 * 1000; } @@ -67,10 +83,26 @@ registerModel({ * @private */ _onTimeoutOwner() { + if (this.callViewAsShowOverlay) { + this.callViewAsShowOverlay.onShowOverlayTimeout(); + return; + } + if (this.chatterOwnerAsAttachmentsLoader) { + this.chatterOwnerAsAttachmentsLoader.onAttachmentsLoadingTimeout(); + return; + } + if (this.messageViewOwnerAsHighlight) { + this.messageViewOwnerAsHighlight.onHighlightTimerTimeout(); + return; + } if (this.messagingOwnerAsFetchImStatusTimer) { this.messagingOwnerAsFetchImStatusTimer.onFetchImStatusTimerTimeout(); return; } + if (this.rtcSessionOwnerAsBroadcast) { + this.rtcSessionOwnerAsBroadcast.onBroadcastTimeout(); + return; + } if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) { this.threadAsCurrentPartnerInactiveTypingTimerOwner.onCurrentPartnerInactiveTypingTimeout(); return; @@ -90,6 +122,14 @@ registerModel({ }, }, fields: { + callViewAsShowOverlay: one('CallView', { + inverse: 'showOverlayTimer', + readonly: true, + }), + chatterOwnerAsAttachmentsLoader: one('Chatter', { + inverse: 'attachmentsLoaderTimer', + readonly: true, + }), /** * Duration, in milliseconds, until timer times out and calls the * timeout function. @@ -103,11 +143,19 @@ registerModel({ inverse: 'fetchImStatusTimer', readonly: true, }), + messageViewOwnerAsHighlight: one('MessageView', { + inverse: 'highlightTimer', + readonly: true, + }), otherMemberLongTypingInThreadTimerOwner: one('OtherMemberLongTypingInThreadTimer', { inverse: 'timer', isCausal: true, readonly: true, }), + rtcSessionOwnerAsBroadcast: one('RtcSession', { + inverse: 'broadcastTimer', + readonly: true, + }), threadAsCurrentPartnerInactiveTypingTimerOwner: one('Thread', { inverse: 'currentPartnerInactiveTypingTimer', readonly: true,