[FIX] mail, im_livechat: improve performance of sidebar item counter

Avoid iterating over all channels every time a new channel is added.

Part of task-2702450

closes odoo/odoo#81264

X-original-commit: 7686be710ba919079f6ea97293ac99dde79f843b
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2021-12-11 10:54:05 +00:00
committed by Alexandre Kühn
parent 26a5d7d299
commit 80232c52db
8 changed files with 104 additions and 30 deletions
@@ -17,6 +17,16 @@ patchRecordMethods('mail.discuss_sidebar_category_item', {
}
return this._super();
},
/**
* @override
*/
_computeCategoryCounterContribution() {
switch (this.channel.channel_type) {
case 'livechat':
return this.channel.localMessageUnreadCounter > 0 ? 1 : 0;
}
return this._super();
},
/**
* @override
*/
@@ -14,7 +14,6 @@ patchRecordMethods('mail.messaging_initializer', {
_initResUsersSettings({ is_discuss_sidebar_category_livechat_open }) {
this.messaging.discuss.update({
categoryLivechat: insertAndReplace({
counterComputeMethod: 'unread',
isServerOpen: is_discuss_sidebar_category_livechat_open,
name: this.env._t("Livechat"),
serverStateKey: 'is_discuss_sidebar_category_livechat_open',
@@ -118,6 +118,16 @@ export class ModelField {
* records.
*/
this.sort = sort;
/**
* Determines whether and how elements of this field should be summed
* into a counter field (only makes sense for relational x2many).
*
* It must contain an array of object with 2 keys: `from` giving the
* name of a field on the relation record that holds the contribution of
* that record to the sum, and `to` giving the name of a field on the
* current record which contains the sum.
*/
this.sumContributions = [];
/**
* This prop only makes sense in a relational field. Determine which
* model name this relation refers to.
@@ -1,5 +1,6 @@
/** @odoo-module */
import { decrement, increment } from '@mail/model/model_field_command';
import { Listener } from '@mail/model/model_listener';
import { followRelations } from '@mail/model/model_utils';
@@ -21,6 +22,14 @@ export class RelationSet {
this.sortArray = new Array();
this.sortListenerByValue = new Map();
}
if (this.field.sumContributions.length > 0) {
this.sumByValueByField = new Map();
this.sumListenerByValueByField = new Map();
for (const { to: sumFieldName } of this.field.sumContributions) {
this.sumByValueByField.set(sumFieldName, new Map());
this.sumListenerByValueByField.set(sumFieldName, new Map());
}
}
}
/**
@@ -103,6 +112,34 @@ export class RelationSet {
this.sortListenerByValue.set(value, listener);
listener.onChange({ reason: 'initial call' });
}
for (const { from: contributionFieldName, to: sumFieldName } of this.field.sumContributions) {
this.sumByValueByField.get(sumFieldName).set(value, 0);
const listener = new Listener({
isPartOfUpdateCycle: true,
name: `sum of field(${sumFieldName}) of ${this.record} from field(${contributionFieldName}) of ${value} through relation ${this.field}`,
onChange: info => {
// listen to the contribution to mark it as dependency
this.record.modelManager.startListening(listener);
const contribution = value[contributionFieldName];
this.record.modelManager.stopListening(listener);
// retrieve the previous contribution and update it
const previousContribution = this.sumByValueByField.get(sumFieldName).get(value);
this.sumByValueByField.get(sumFieldName).set(value, contribution);
this.record.modelManager._update(
this.record,
{
[sumFieldName]: [
decrement(previousContribution),
increment(contribution),
],
},
{ allowWriteReadonly: true },
);
},
});
this.sumListenerByValueByField.get(sumFieldName).set(value, listener);
listener.onChange({ reason: 'initial call' });
}
}
/**
@@ -132,6 +169,16 @@ export class RelationSet {
this.sortListenerByValue.delete(listener);
this.record.modelManager.removeListener(listener);
}
for (const { to: sumFieldName } of this.field.sumContributions) {
// remove contribution of current value
const contribution = this.sumByValueByField.get(sumFieldName).get(value);
this.sumByValueByField.get(sumFieldName).delete(value);
this.record.modelManager._update(this.record, { [sumFieldName]: decrement(contribution) }, { allowWriteReadonly: true });
// remove listener on current value
const listener = this.sumListenerByValueByField.get(sumFieldName).get(value);
this.sumListenerByValueByField.get(sumFieldName).delete(value);
this.record.modelManager.removeListener(listener);
}
return true;
}
@@ -490,6 +490,7 @@ export class ModelManager {
'readonly',
'related',
'required',
'sum',
].includes(key)
);
if (invalidKeys.length > 0) {
@@ -1176,11 +1177,25 @@ export class ModelManager {
* 1. Prepare fields.
*/
for (const Model of Object.values(this.models)) {
const sumContributionsByFieldName = new Map();
// Make fields aware of their field name.
for (const [fieldName, fieldData] of registry.get(Model.name).get('fields')) {
Model.fields[fieldName] = new ModelField(Object.assign({}, fieldData, {
fieldName,
}));
if (fieldData.sum) {
const [relationFieldName, contributionFieldName] = fieldData.sum.split('.');
if (!sumContributionsByFieldName.has(relationFieldName)) {
sumContributionsByFieldName.set(relationFieldName, []);
}
sumContributionsByFieldName.get(relationFieldName).push({
from: contributionFieldName,
to: fieldName,
});
}
}
for (const [fieldName, sumContributions] of sumContributionsByFieldName) {
Model.fields[fieldName].sumContributions = sumContributions;
}
}
/**
@@ -73,18 +73,6 @@ registerModel({
}
return clear();
},
/**
* @private
* @returns {integer}
*/
_computeCounter() {
switch (this.counterComputeMethod) {
case 'needaction':
return this.categoryItems.filter(categoryItem => categoryItem.channel.message_needaction_counter > 0).length;
case 'unread':
return this.categoryItems.filter(categoryItem => categoryItem.channel.localMessageUnreadCounter > 0).length;
}
},
/**
* @private
* @returns {FieldCommand}
@@ -237,17 +225,13 @@ registerModel({
sort: '_sortDefinitionCategoryItems',
}),
/**
* The total amount unread/action-needed threads in the category.
* States the total amount of unread/action-needed threads in this
* category.
*/
counter: attr({
compute: '_computeCounter',
}),
/**
* Determines how the counter of this category should be computed.
* Supported methods: 'needaction', 'unread'.
*/
counterComputeMethod: attr({
required: true,
default: 0,
readonly: true,
sum: 'categoryItems.categoryCounterContribution',
}),
discussAsChannel: one2one('mail.discuss', {
inverse: 'categoryChannel',
@@ -33,13 +33,16 @@ registerModel({
},
/**
* @private
* @returns {mail.thread}
* @returns {integer}
*/
_computeChannel() {
return link(this.messaging.models['mail.thread'].findFromIdentifyingData({
id: this.channel.id,
model: 'mail.channel',
}));
_computeCategoryCounterContribution() {
switch (this.channel.channel_type) {
case 'channel':
return this.channel.message_needaction_counter > 0 ? 1 : 0;
case 'chat':
case 'group':
return this.channel.localMessageUnreadCounter > 0 ? 1 : 0;
}
},
/**
* @private
@@ -219,6 +222,14 @@ registerModel({
readonly: true,
required: true,
}),
/**
* Determines the contribution of this discuss sidebar category item to
* the counter of this category.
*/
categoryCounterContribution: attr({
compute: '_computeCategoryCounterContribution',
readonly: true,
}),
/**
* Amount of unread/action-needed messages
*/
@@ -244,7 +244,6 @@ registerModel({
categoryChannel: insertAndReplace({
autocompleteMethod: 'channel',
commandAddTitleText: this.env._t("Add or join a channel"),
counterComputeMethod: 'needaction',
hasAddCommand: true,
hasViewCommand: true,
isServerOpen: is_discuss_sidebar_category_channel_open,
@@ -257,7 +256,6 @@ registerModel({
categoryChat: insertAndReplace({
autocompleteMethod: 'chat',
commandAddTitleText: this.env._t("Start a conversation"),
counterComputeMethod: 'unread',
hasAddCommand: true,
isServerOpen: is_discuss_sidebar_category_chat_open,
name: this.env._t("Direct Messages"),