[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:
committed by
Sébastien Theys
parent
be2f050993
commit
3dd9c3b7f9
@@ -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
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+53
@@ -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',
|
||||
|
||||
+7
-2
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}"`);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user