[IMP] mail: Introduce view models

This commit moves code from components to models,
by introduce some models and fields.

It also cleans a few things in models, like:
- use Timer model instead of setTimeout
- replace local variables by model fields

Task-2881075

All changes:
- [IMP] mail: remove ChannelMemberListCategoryComponent/channelMemberListView
Task-2831082
- [IMP] mail: introduce ChannelMemberListCategoryView/members
Task-2864112
- [IMP] mail: introduce ChannelMemberListCategoryView/title
Task-2864115
- [IMP] mail: introduce ChannelMemberView
Task-2864119
- [IMP] mail: introduce 'Chatter/attachmentsLoaderTimer'
Task-2868781
- [IMP] mail: introduce 'MessageView/highlightTimer'
Task-2868794
- [IMP] mail: introduce 'CallView/showOverlayTimer'
Task-2868867
- [IMP] mail: introduce RtcSession/broadcastTimer
Task-2868929
- [IMP] mail: introduce CallView/tileContainerRef
Task-2871859
- [IMP] mail: introduce CallView/tileWidth
Task-2871869
- [IMP] mail: introduce CallView/tileHeight
Task-2871878
- [IMP] mail: introduce ChatWindowManager/endGapWidth
Task-2871960
- [IMP] mail: remove unused CallView/columnCount
Task-2871885
- [IMP] mail: introduce ChatWindowManager/hiddenMenuWidth
Task-2871963
- [IMP] mail: introduce CallView/calculateTessellation
Task-2871867
- [IMP] mail: replace model methods of MediaPreview by record methods
Task-2871934
- [IMP] mail: introduce ChatWindowManager/betweenGapWidth
Task-2871956
- [IMP] mail: introduce ChatWindowManager/chatWindowWidth
Task-2871957
- [IMP] mail: introduce ChatWindowManager/startGapWidth
Task-2871965
- [IMP] mail: introduce ChatWindowManager/availableVisibleSlots
Task-2871971
- [IMP] mail: introduce ChatWindowManager/visual/hiddenChatWindowLocalIds
Task-2871980
- [IMP] mail: introduce ChatWindowManager/visual/isHiddenMenuVisible
Task-2871981
- [IMP] mail: introduce ChatWindowManager/visual/hiddenMenuOffset
Task-2871984

Part-of: odoo/odoo#93330
This commit is contained in:
Alexandre Kühn
2022-06-11 02:15:15 +02:00
parent eca952307a
commit dcfc8a91e8
19 changed files with 377 additions and 262 deletions
@@ -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,
});
}
/**
@@ -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;"
>
<t t-foreach="callView.tileParticipantCards" t-as="participantCard" t-key="'grid_tile_'+participantCard.localId">
<t t-if="!callView.filterVideoGrid or (participantCard.rtcSession and participantCard.rtcSession.videoStream)">
@@ -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',
});
@@ -2,23 +2,23 @@
<templates xml:space="preserve">
<t t-name="mail.ChannelMember" owl="1">
<div class="o_ChannelMember d-flex align-items-center mx-3 my-1" t-attf-class="{{ className }}" t-att-data-partner-local-id="member.localId" t-ref="root">
<div class="o_ChannelMember_avatarContainer position-relative flex-shrink-0 o_cursor_pointer" t-on-click="() => channel.onClickMemberAvatar(member)">
<img class="o_ChannelMember_avatar rounded-circle w-100 h-100" t-attf-src="/mail/channel/{{ channel.id }}/partner/{{ member.id }}/avatar_128" alt="Avatar"/>
<div class="o_ChannelMember d-flex align-items-center mx-3 my-1" t-attf-class="{{ className }}" t-att-data-partner-local-id="record.partner.localId" t-ref="root">
<div class="o_ChannelMember_avatarContainer position-relative flex-shrink-0 o_cursor_pointer" t-on-click="() => channel.onClickMemberAvatar(record.partner)">
<img class="o_ChannelMember_avatar rounded-circle w-100 h-100" t-attf-src="/mail/channel/{{ channel.id }}/partner/{{ record.partner.id }}/avatar_128" alt="Avatar"/>
<t t-if="member.im_status and member.im_status !== 'im_partner'">
<t t-if="record.partner.im_status and record.partner.im_status !== 'im_partner'">
<PartnerImStatusIcon
className="'o_ChannelMember_partnerImStatusIcon position-absolute bottom-0 end-0 d-flex align-items-center justify-content-center text-light'"
classNameObj="{
'o-isDeviceSmall': messaging.device.isSmall,
'small': !messaging.device.isSmall,
}"
partner="member"
partner="record.partner"
/>
</t>
</div>
<span class="o_ChannelMember_name ml-2 flex-column-1 text-truncate o_cursor_pointer" t-on-click="() => channel.onClickMemberName(member)">
<t t-esc="member.nameOrDisplayName"/>
<span class="o_ChannelMember_name ml-2 flex-column-1 text-truncate o_cursor_pointer" t-on-click="() => channel.onClickMemberName(record.partner)">
<t t-esc="record.partner.nameOrDisplayName"/>
</span>
</div>
</t>
@@ -5,10 +5,10 @@
<t t-if="channelMemberListView">
<div class="o_ChannelMemberList d-flex flex-column overflow-auto bg-light" t-attf-class="{{ className }}" t-ref="root">
<t t-if="channelMemberListView.onlineCategoryView">
<ChannelMemberListCategory record="channelMemberListView.onlineCategoryView" channelMemberListView="channelMemberListView"/>
<ChannelMemberListCategory record="channelMemberListView.onlineCategoryView"/>
</t>
<t t-if="channelMemberListView.offlineCategoryView">
<ChannelMemberListCategory record="channelMemberListView.offlineCategoryView" channelMemberListView="channelMemberListView"/>
<ChannelMemberListCategory record="channelMemberListView.offlineCategoryView"/>
</t>
<t t-if="channelMemberListView.channel.unknownMemberCount === 1">
<span class="mx-2 mt-2">And 1 other member.</span>
@@ -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',
});
@@ -2,9 +2,9 @@
<templates xml:space="preserve">
<t t-name="mail.ChannelMemberListCategory" owl="1">
<h6 class="m-2" t-esc="title"/>
<t t-foreach="members" t-as="member" t-key="member.localId">
<ChannelMember channel="channelMemberListView.channel" member="member"/>
<h6 class="m-2" t-esc="record.title"/>
<t t-foreach="record.channelMemberViews" t-as="channelMemberView" t-key="channelMemberView.localId">
<ChannelMember channel="record.channel" record="channelMemberView"/>
</t>
</t>
@@ -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';
}
@@ -4,7 +4,7 @@
<t t-name="mail.ChatWindowManager" owl="1">
<div class="o_ChatWindowManager flex-row-reverse d-flex" t-attf-class="{{ className }}" t-ref="root">
<!-- Note: DOM elements are ordered from left to right -->
<t t-if="messaging.chatWindowManager.visual.hidden.isVisible">
<t t-if="messaging.chatWindowManager.visual.isHiddenMenuVisible">
<ChatWindowHiddenMenu className="'o_ChatWindowManager_hiddenMenu'"/>
</t>
<t t-foreach="messaging.chatWindowManager.allOrderedVisible" t-as="chatWindow" t-key="chatWindow.localId">
+59 -16
View File
@@ -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({
@@ -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',
}),
},
});
@@ -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,
}),
},
});
@@ -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',
}),
+10 -24
View File
@@ -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.
*/
+13 -15
View File
@@ -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}
@@ -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.)
+4
View File
@@ -376,6 +376,10 @@ registerModel({
inverse: 'partner',
isCausal: true,
}),
channelMemberViews: many('ChannelMemberView', {
inverse: 'partner',
isCausal: true,
}),
country: one('Country'),
/**
* Deprecated.
+25 -33
View File
@@ -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
+48
View File
@@ -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,