[FIX] mail: handle transient messages with (un)seen notions

task-2343887

closes odoo/odoo#61180

X-original-commit: 03481fd3df9bc53a17ba884ad4029ae46d7e54bf
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Xavier Dubuc
2020-11-02 14:56:57 +00:00
committed by Sébastien Theys
parent be2f050993
commit 3dd9c3b7f9
14 changed files with 423 additions and 107 deletions
@@ -1932,7 +1932,6 @@ QUnit.test('new message separator is shown in a chat window of a chat on receivi
is_minimized: true,
is_pinned: false,
members: [this.data.currentPartnerId, 10],
message_unread_counter: 0,
uuid: 'channel-10-uuid',
},
];
@@ -1890,7 +1890,16 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
QUnit.test('restore thread scroll position', async function (assert) {
assert.expect(6);
// channels expected to be rendered, with random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 11 }, { id: 12 });
this.data['mail.channel'].records.push(
{
id: 11,
seen_message_id: 50, // prevent race condition with mark as seen (task-2372339)
},
{
id: 12,
seen_message_id: 50, // prevent race condition with mark as seen (task-2372339)
},
);
for (let i = 1; i <= 25; i++) {
this.data['mail.message'].records.push({
body: "not empty",
@@ -7,7 +7,7 @@
<t t-if="message.author and message.author === env.messaging.currentPartner">
<i class="o_MessageAuthorPrefixIcon fa fa-mail-reply"/>You:
</t>
<t t-elif="thread and thread.channel_type === 'channel'">
<t t-elif="thread and message.author !== thread.correspondent">
<t t-esc="message.author.nameOrDisplayName"/>:
</t>
</t>
@@ -43,12 +43,12 @@
<!-- MESSAGES -->
<t t-set="current_day" t-value="0"/>
<t t-set="prev_message" t-value="0"/>
<t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and !threadView.thread.lastSeenByCurrentPartnerMessageId">
<div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
<hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
</div>
</t>
<t t-foreach="orderedMessages" t-as="message" t-key="message.localId">
<t t-if="message === threadView.thread.messageAfterNewMessageSeparator">
<div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
<hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
</div>
</t>
<t t-if="!message.isEmpty">
<t t-set="message_day" t-value="getDateDay(message)"/>
<t t-if="current_day !== message_day">
@@ -76,11 +76,6 @@
t-ref="{{ message.localId }}"
/>
<t t-set="prev_message" t-value="message"/>
<t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and threadView.thread.lastSeenByCurrentPartnerMessageId === message.id">
<div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
<hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
</div>
</t>
</t>
</t>
<!-- LOADING (if order desc)-->
@@ -289,6 +289,10 @@ QUnit.test('basic rendering', async function (assert) {
QUnit.test('counter is taking into account failure notification', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
id: 31,
seen_message_id: 11,
});
// message that is expected to have a failure
this.data['mail.message'].records.push({
id: 11, // random unique id, will be used to link failure to message
@@ -39,9 +39,9 @@
</div>
<div class="o_ThreadNeedactionPreview_core">
<span class="o_ThreadNeedactionPreview_coreItem o_ThreadNeedactionPreview_inlineText" t-att-class="{ 'o-empty': inlineLastNeedactionMessageBody.length === 0 }">
<t t-if="thread.lastMessage and thread.lastMessage.author">
<t t-if="thread.lastNeedactionMessage and thread.lastNeedactionMessage.author">
<MessageAuthorPrefix
messageLocalId="thread.lastMessage.localId"
messageLocalId="thread.lastNeedactionMessage.localId"
threadLocalId="thread.localId"
/>
</t>
@@ -292,6 +292,59 @@ QUnit.test('[technical] opening a non-channel chat window should not call channe
);
});
QUnit.test('preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread', async function (assert) {
assert.expect(2);
this.data['res.partner'].records.push({
id: 11,
name: "Stranger",
});
this.data['mail.message'].records.push({
author_id: 11,
body: "I am the oldest but needaction",
id: 21,
model: 'res.partner',
needaction: true,
needaction_partner_ids: [this.data.currentPartnerId],
res_id: 11,
});
this.data['mail.message'].records.push({
author_id: this.data.currentPartnerId,
body: "I am more recent",
id: 22,
model: 'res.partner',
res_id: 11,
});
this.data['mail.notification'].records.push({
mail_message_id: 21,
notification_status: 'sent',
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
});
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
message: "should wait until inbox loaded initial needaction messages",
predicate: ({ threadCache }) => {
return threadCache.thread.model === 'mail.box' && threadCache.thread.id === 'inbox';
},
}));
assert.containsOnce(
document.body,
'.o_ThreadNeedactionPreview_inlineText',
"should have a preview from the last message"
);
assert.strictEqual(
document.querySelector('.o_ThreadNeedactionPreview_inlineText').textContent,
'Stranger: I am the oldest but needaction',
"the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread"
);
});
});
});
});
@@ -108,8 +108,8 @@ class ThreadPreview extends Component {
* @param {MouseEvent} ev
*/
_onClickMarkAsRead(ev) {
if (this.thread.lastMessage) {
this.thread.markAsSeen(this.thread.lastMessage.id);
if (this.thread.lastNonTransientMessage) {
this.thread.markAsSeen(this.thread.lastNonTransientMessage);
}
}
@@ -1367,6 +1367,80 @@ QUnit.test('[technical] message list with a full page of empty messages should s
);
});
QUnit.test('first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]', async function (assert) {
// The goal of removing the focus is to ensure the thread is not marked as seen automatically.
// Indeed that would trigger channel_seen no matter what, which is already covered by other tests.
// The goal of this test is to cover the conditions specific to transient messages,
// and the conditions from focus would otherwise shadow them.
assert.expect(3);
this.data['mail.channel_command'].records.push({ name: 'who' });
// Needed partner & user to allow simulation of message reception
this.data['res.partner'].records.push({
id: 11,
name: "Foreigner partner",
});
this.data['res.users'].records.push({
id: 42,
name: "Foreigner user",
partner_id: 11,
});
this.data['mail.channel'].records = [{
channel_type: 'channel',
id: 20,
is_pinned: true,
name: "General",
uuid: 'channel20uuid',
}];
await this.start();
const thread = this.env.models['mail.thread'].findFromIdentifyingData({
id: 20,
model: 'mail.channel'
});
const threadViewer = this.env.models['mail.thread_viewer'].create({
hasThreadView: true,
thread: [['link', thread]],
});
await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
// send a command that leads to receiving a transient message
document.querySelector('.o_ComposerTextInput_textarea').focus();
await afterNextRender(() => document.execCommand('insertText', false, "/who"));
await afterNextRender(() => {
document.querySelector('.o_Composer_buttonSend').focus();
document.querySelector('.o_Composer_buttonSend').click();
});
// simulate receiving a message
await afterNextRender(() => this.env.services.rpc({
route: '/mail/chat_post',
params: {
context: {
mockedUserId: 42,
},
uuid: 'channel20uuid',
message_content: "test",
},
}));
assert.containsN(
document.body,
'.o_Message',
2,
"should display 2 messages (the transient & the received message), after posting a command"
);
assert.containsOnce(
document.body,
'.o_MessageList_separatorNewMessages',
"separator should be shown as a message has been received"
);
assert.containsOnce(
document.body,
`.o_Message[data-message-local-id="${
this.env.models['mail.message'].find(m => m.isTransient).localId
}"] + .o_MessageList_separatorNewMessages`,
"separator should be shown just after transient message"
);
});
});
});
});
@@ -269,25 +269,34 @@ function factory(dependencies) {
if (thread.model === 'mail.channel') {
const command = this._getCommandFromText(body);
Object.assign(postData, {
command: command ? command.name : undefined,
subtype_xmlid: 'mail.mt_comment'
subtype_xmlid: 'mail.mt_comment',
});
messageId = await this.async(() => this.env.services.rpc({
model: 'mail.channel',
method: command ? 'execute_command' : 'message_post',
args: [thread.id],
kwargs: postData,
}));
if (command) {
messageId = await this.async(() => this.env.models['mail.thread'].performRpcExecuteCommand({
channelId: thread.id,
command: command.name,
postData,
}));
} else {
messageId = await this.async(() =>
this.env.models['mail.thread'].performRpcMessagePost({
postData,
threadId: thread.id,
threadModel: thread.model,
})
);
}
} else {
Object.assign(postData, {
subtype_xmlid: this.isLog ? 'mail.mt_note' : 'mail.mt_comment',
});
messageId = await this.async(() => this.env.services.rpc({
model: thread.model,
method: 'message_post',
args: [thread.id],
kwargs: postData,
}));
messageId = await this.async(() =>
this.env.models['mail.thread'].performRpcMessagePost({
postData,
threadId: thread.id,
threadModel: thread.model,
})
);
const [messageData] = await this.async(() => this.env.services.rpc({
model: 'mail.message',
method: 'message_format',
@@ -655,14 +655,19 @@ function factory(dependencies) {
*/
_handleNotificationPartnerTransientMessage(data) {
const convertedData = this.env.models['mail.message'].convertData(data);
const messageIds = this.env.models['mail.message'].all().map(message => message.id);
const lastMessageId = this.env.models['mail.message'].all().reduce(
(lastMessageId, message) => Math.max(lastMessageId, message.id),
0
);
const partnerRoot = this.env.messaging.partnerRoot;
const message = this.env.models['mail.message'].create(Object.assign(convertedData, {
author: [['link', partnerRoot]],
id: (messageIds ? Math.max(...messageIds) : 0) + 0.01,
id: lastMessageId + 0.01,
isTransient: true,
}));
this._notifyThreadViewsMessageReceived(message);
// manually force recompute of counter
this.messaging.messagingMenu.update();
}
/**
+193 -62
View File
@@ -197,7 +197,7 @@ function factory(dependencies) {
data2.public = data.public;
}
if ('seen_message_id' in data) {
data2.lastSeenByCurrentPartnerMessageId = data.seen_message_id;
data2.lastSeenByCurrentPartnerMessageId = data.seen_message_id || 0;
}
if ('uuid' in data) {
data2.uuid = data.uuid;
@@ -475,17 +475,35 @@ function factory(dependencies) {
*
* @static
* @param {Object} param0
* @param {string} param0.command the command to execute
* @param {integer[]} param0.ids list of id of channels
* @param {integer} param0.channelId
* @param {string} param0.command
* @param {Object} [param0.postData={}]
*/
static async performRpcExecuteCommand({ command, ids }) {
static async performRpcExecuteCommand({ channelId, command, postData = {} }) {
return this.env.services.rpc({
model: 'mail.channel',
method: 'execute_command',
args: [ids],
kwargs: {
command,
}
args: [[channelId]],
kwargs: Object.assign({ command }, postData),
});
}
/**
* Performs the `message_post` RPC on given threadModel.
*
* @static
* @param {Object} param0
* @param {Object} param0.postData
* @param {integer} param0.threadId
* @param {string} param0.threadModel
* @return {integer} the posted message id
*/
static async performRpcMessagePost({ postData, threadId, threadModel }) {
return this.env.services.rpc({
model: threadModel,
method: 'message_post',
args: [threadId],
kwargs: postData,
});
}
@@ -614,26 +632,25 @@ function factory(dependencies) {
/**
* Mark the specified conversation as read/seen.
*
* @param {integer} messageId the message to be considered as last seen
* @param {mail.message} message the message to be considered as last seen.
*/
async markAsSeen(messageId) {
async markAsSeen(message) {
if (this.model !== 'mail.channel') {
return;
}
if (this.pendingSeenMessageId && messageId <= this.pendingSeenMessageId) {
if (this.pendingSeenMessageId && message.id <= this.pendingSeenMessageId) {
return;
}
if (
this.lastSeenByCurrentPartnerMessageId &&
messageId <= this.lastSeenByCurrentPartnerMessageId
message.id <= this.lastSeenByCurrentPartnerMessageId
) {
return;
}
this.update({ pendingSeenMessageId: messageId });
this.update({ pendingSeenMessageId: message.id });
return this.env.models['mail.thread'].performRpcChannelSeen({
ids: [this.id],
// commands have fake message id that is not integer
lastMessageId: Math.floor(messageId),
lastMessageId: message.id,
});
}
@@ -676,9 +693,9 @@ function factory(dependencies) {
uuid: this.uuid,
});
} else {
await this.env.models['mail.thread'].performRpcExecuteCommand({
this.env.models['mail.thread'].performRpcExecuteCommand({
channelId: this.id,
command: 'leave',
ids: [this.id],
});
}
}
@@ -1105,9 +1122,6 @@ function factory(dependencies) {
* @returns {mail.message}
*/
_computeLastCurrentPartnerMessageSeenByEveryone() {
if (!this.partnerSeenInfos || !this.orderedMessages) {
return [['unlink-all']];
}
const otherPartnerSeenInfos =
this.partnerSeenInfos.filter(partnerSeenInfo =>
partnerSeenInfo.partner !== this.messagingCurrentPartner);
@@ -1126,7 +1140,7 @@ function factory(dependencies) {
...otherPartnersLastSeenMessageIds
);
const currentPartnerOrderedSeenMessages =
this.orderedMessages.filter(message =>
this.orderedNonTransientMessages.filter(message =>
message.author === this.messagingCurrentPartner &&
message.id <= lastMessageSeenByAllId);
@@ -1154,6 +1168,56 @@ function factory(dependencies) {
return [['unlink']];
}
/**
* @private
* @returns {mail.message|undefined}
*/
_computeLastNonTransientMessage() {
const {
length: l,
[l - 1]: lastMessage,
} = this.orderedNonTransientMessages;
if (lastMessage) {
return [['link', lastMessage]];
}
return [['unlink']];
}
/**
* Adjusts the last seen message received from the server to consider
* the following messages also as read if they are either transient
* messages or messages from the current partner.
*
* @private
* @returns {integer}
*/
_computeLastSeenByCurrentPartnerMessageId() {
const firstMessage = this.orderedMessages[0];
if (
firstMessage &&
this.lastSeenByCurrentPartnerMessageId &&
this.lastSeenByCurrentPartnerMessageId < firstMessage.id
) {
// no deduction can be made if there is a gap
return this.lastSeenByCurrentPartnerMessageId;
}
let lastSeenByCurrentPartnerMessageId = this.lastSeenByCurrentPartnerMessageId;
for (const message of this.orderedMessages) {
if (message.id <= this.lastSeenByCurrentPartnerMessageId) {
continue;
}
if (
message.author === this.env.messaging.currentPartner ||
message.isTransient
) {
lastSeenByCurrentPartnerMessageId = message.id;
continue;
}
return lastSeenByCurrentPartnerMessageId;
}
return lastSeenByCurrentPartnerMessageId;
}
/**
* @private
* @returns {mail.message|undefined}
@@ -1185,41 +1249,31 @@ function factory(dependencies) {
* @returns {integer}
*/
_computeLocalMessageUnreadCounter() {
// the only situations where serverMessageUnreadCounter can/have to be
// trusted are:
// - we have no last message (and then no messages at all)
// - the message it used to compute is the last message we know
if (this.orderedMessages.length === 0) {
return this.serverMessageUnreadCounter;
}
// from here serverLastMessageId is not undefined because
// orderedMessages contain at least one message.
if (!this.lastSeenByCurrentPartnerMessageId) {
return this.serverMessageUnreadCounter;
}
// if server knows more messages than the client knows then we just
// need to trust him
if (this.serverLastMessageId > this.lastSeenByCurrentPartnerMessageId) {
return this.serverMessageUnreadCounter;
}
// By default trust the server up to the last message it used
// because it's not possible to do better.
let baseCounter = this.serverMessageUnreadCounter;
let countFromId = this.serverLastMessageId;
// But if the client knows the last seen message that the server
// returned (and by assumption all the messages that come after),
// the counter can be computed fully locally, ignoring potentially
// obsolete values from the server.
const firstMessage = this.orderedMessages[0];
// if the lastSeenByCurrentPartnerMessageId is not known (not fetched), then we
// need to rely on server value to determine the amount of unread
// messages until the last message it knew when computing the
// serverMessageUnreadCounter
if (this.lastSeenByCurrentPartnerMessageId < firstMessage.id) {
const fetchedNotSeenMessages = this.orderedMessages.filter(message =>
message.id > this.serverLastMessageId && message.author !== this.env.messaging.currentPartner
);
return this.serverMessageUnreadCounter + fetchedNotSeenMessages.length;
if (
firstMessage &&
this.lastSeenByCurrentPartnerMessageId &&
this.lastSeenByCurrentPartnerMessageId >= firstMessage.id
) {
baseCounter = 0;
countFromId = this.lastSeenByCurrentPartnerMessageId;
}
// lastSeenByCurrentPartnerMessageId is a known message,
// then we can forget serverMessageUnreadCounter
const maxId = this.lastSeenByCurrentPartnerMessageId;
return this.orderedMessages.reduce(
(acc, message) => acc + ((message.id > maxId && message.author !== this.env.messaging.currentPartner) ? 1 : 0),
0
);
// Include all the messages that are known locally but the server
// didn't take into account.
return this.orderedMessages.reduce((total, message) => {
if (message.id <= countFromId) {
return total;
}
return total + 1;
}, baseCounter);
}
/**
@@ -1238,6 +1292,27 @@ function factory(dependencies) {
return [['replace', this.messages.filter(message => message.isNeedaction)]];
}
/**
* @private
* @returns {mail.message|undefined}
*/
_computeMessageAfterNewMessageSeparator() {
if (this.model !== 'mail.channel') {
return [['unlink']];
}
if (this.localMessageUnreadCounter === 0) {
return [['unlink']];
}
const index = this.orderedMessages.findIndex(message =>
message.id === this.lastSeenByCurrentPartnerMessageId
);
const message = this.orderedMessages[index + 1];
if (!message) {
return [['unlink']];
}
return [['link', message]];
}
/**
* @private
* @returns {mail.message[]}
@@ -1246,6 +1321,14 @@ function factory(dependencies) {
return [['replace', this.messages.sort((m1, m2) => m1.id < m2.id ? -1 : 1)]];
}
/**
* @private
* @returns {mail.message[]}
*/
_computeOrderedNonTransientMessages() {
return [['replace', this.orderedMessages.filter(m => !m.isTransient)]];
}
/**
* @private
* @returns {mail.partner[]}
@@ -1638,11 +1721,14 @@ function factory(dependencies) {
lastCurrentPartnerMessageSeenByEveryone: many2one('mail.message', {
compute: '_computeLastCurrentPartnerMessageSeenByEveryone',
dependencies: [
'partnerSeenInfos',
'orderedMessages',
'messagingCurrentPartner',
'orderedNonTransientMessages',
'partnerSeenInfos',
],
}),
/**
* Last message of the thread, could be a transient one.
*/
lastMessage: many2one('mail.message', {
compute: '_computeLastMessage',
dependencies: ['orderedMessages'],
@@ -1651,18 +1737,31 @@ function factory(dependencies) {
compute: '_computeLastNeedactionMessage',
dependencies: ['needactionMessages'],
}),
/**
* Last non-transient message.
*/
lastNonTransientMessage: many2one('mail.message', {
compute: '_computeLastNonTransientMessage',
dependencies: ['orderedNonTransientMessages'],
}),
/**
* Last seen message id of the channel by current partner.
*
* If there is a pending seen message id change, it is immediately applied
* on the interface to avoid a feeling of unresponsiveness. Otherwise the
* last known message id of the server is used.
*
* Also, it needs to be kept as an id because it's considered like a "date" and could stay
* even if corresponding message is deleted. It is basically used to know which
* messages are before or after it.
*/
lastSeenByCurrentPartnerMessageId: attr(),
lastSeenByCurrentPartnerMessageId: attr({
compute: '_computeLastSeenByCurrentPartnerMessageId',
default: 0,
dependencies: [
'lastSeenByCurrentPartnerMessageId',
'messagingCurrentPartner',
'orderedMessages',
'orderedMessagesIsTransient',
// FIXME missing dependency 'orderedMessages.author', (task-2261221)
],
}),
/**
* Local value of message unread counter, that means it is based on initial server value and
* updated with interface updates.
@@ -1670,7 +1769,6 @@ function factory(dependencies) {
localMessageUnreadCounter: attr({
compute: '_computeLocalMessageUnreadCounter',
dependencies: [
'lastMessage',
'lastSeenByCurrentPartnerMessageId',
'messagingCurrentPartner',
'orderedMessages',
@@ -1687,6 +1785,19 @@ function factory(dependencies) {
members: many2many('mail.partner', {
inverse: 'memberThreads',
}),
/**
* Determines the message before which the "new message" separator must
* be positioned, if any.
*/
messageAfterNewMessageSeparator: many2one('mail.message', {
compute: '_computeMessageAfterNewMessageSeparator',
dependencies: [
'lastSeenByCurrentPartnerMessageId',
'localMessageUnreadCounter',
'model',
'orderedMessages',
],
}),
message_needaction_counter: attr({
default: 0,
}),
@@ -1752,10 +1863,30 @@ function factory(dependencies) {
'serverFoldState',
],
}),
/**
* All messages ordered like they are displayed.
*/
orderedMessages: many2many('mail.message', {
compute: '_computeOrderedMessages',
dependencies: ['messages'],
}),
/**
* Serves as compute dependency. (task-2261221)
*/
orderedMessagesIsTransient: attr({
related: 'orderedMessages.isTransient',
}),
/**
* All messages ordered like they are displayed. This field does not
* contain transient messages which are not "real" records.
*/
orderedNonTransientMessages: many2many('mail.message', {
compute: '_computeOrderedNonTransientMessages',
dependencies: [
'orderedMessages',
'orderedMessagesIsTransient',
],
}),
/**
* Ordered typing members on this thread, excluding the current partner.
*/
@@ -114,18 +114,29 @@ function factory(dependencies) {
* @returns {boolean}
*/
_computeThreadShouldBeSetAsSeen() {
// FIXME condition should not be on "composer is focused" but "threadView is active"
// See task-2277543
const lastMessageIsVisible = this.lastVisibleMessage &&
this.lastVisibleMessage === this.lastMessage;
if (lastMessageIsVisible && this.hasComposerFocus && this.thread) {
this.thread.markAsSeen(this.lastMessage.id).catch(e => {
// prevent crash when executing compute during destroy
if (!(e instanceof RecordDeletedError)) {
throw e;
}
});
if (!this.thread) {
return;
}
if (!this.thread.lastNonTransientMessage) {
return;
}
if (!this.lastVisibleMessage) {
return;
}
if (this.lastVisibleMessage !== this.lastMessage) {
return;
}
if (!this.hasComposerFocus) {
// FIXME condition should not be on "composer is focused" but "threadView is active"
// See task-2277543
return;
}
this.thread.markAsSeen(this.thread.lastNonTransientMessage).catch(e => {
// prevent crash when executing compute during destroy
if (!(e instanceof RecordDeletedError)) {
throw e;
}
});
}
/**
@@ -232,6 +243,12 @@ function factory(dependencies) {
lastMessage: many2one('mail.message', {
related: 'thread.lastMessage',
}),
/**
* Serves as compute dependency.
*/
lastNonTransientMessage: many2one('mail.message', {
related: 'thread.lastNonTransientMessage',
}),
/**
* Most recent message in this ThreadView that has been shown to the
* current partner in the currently displayed thread cache.
@@ -329,6 +346,7 @@ function factory(dependencies) {
dependencies: [
'hasComposerFocus',
'lastMessage',
'lastNonTransientMessage',
'lastVisibleMessage',
'threadCache',
],
@@ -851,7 +851,8 @@ MockServer.include({
* @private
*/
_mockMailChannelExecuteCommand(args) {
const [ids, commandName] = args.args;
const ids = args.args[0];
const commandName = args.kwargs.command || args.args[1];
const channels = this._getRecords('mail.channel', [['id', 'in', ids]]);
if (commandName === 'leave') {
for (const channel of channels) {
@@ -866,6 +867,24 @@ MockServer.include({
this._widget.call('bus_service', 'trigger', 'notification', [notifConfirmUnpin]);
}
return;
} else if (commandName === 'who') {
for (const channel of channels) {
const members = channel.members.map(memberId => this._getRecords('res.partner', [['id', '=', memberId]])[0].name);
let message = "You are alone in this channel.";
if (members.length > 0) {
message = `Users in this channel: ${members.join(', ')} and you`
}
const notification = [
["dbName", 'res.partner', this.currentPartnerId],
{
'body': `<span class="o_mail_notification">${message}</span>`,
'channel_ids': [channel.id],
'info': 'transient_message',
}
];
this._widget.call('bus_service', 'trigger', 'notification', [notification]);
}
return;
}
throw new Error(`mail/mock_server: the route execute_command doesn't implement the command "${commandName}"`);
},