[REF] mail: clean message fetch code

Adapt code of message fetch so that fetching of
(needaction) messages are controlled in a way to
keep continuous sequence of messages at all time.

This code design solves the "holes" problem in a message
list by not allowing holes. This fixes many scenario that
generated these holes, resulting in unfetchable messages.

Example of such a scenario:
- post 40 messages
- post a new message that is reply of the oldest message
- page reload and scroll up until load more
=> 10 messages are missing in-between replied message and
 following message (that should have been 11 messages apart)

This code also prepares for allowing jumps in a conversation.

Part-of: odoo/odoo#116666
This commit is contained in:
Alexandre Kühn
2023-04-13 04:08:11 +02:00
parent 05fd65e855
commit fc9d46f3b9
27 changed files with 353 additions and 305 deletions
@@ -44,14 +44,14 @@ patch(ThreadService.prototype, "im_livechat", {
canUnpin(thread) {
if (thread.type === "livechat") {
return this.localMessageUnreadCounter(thread) === 0;
return thread.message_unread_counter === 0;
}
return this._super(thread);
},
getCounter(thread) {
if (thread.type === "livechat") {
return this.localMessageUnreadCounter(thread);
return thread.message_needaction_counter;
}
return this._super(thread);
},
+10 -10
View File
@@ -213,16 +213,16 @@ class DiscussController(http.Controller):
# --------------------------------------------------------------------------
@http.route('/mail/inbox/messages', methods=['POST'], type='json', auth='user')
def discuss_inbox_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], max_id=max_id, min_id=min_id, limit=limit).message_format()
def discuss_inbox_messages(self, before=None, after=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', True)], before=before, after=after, limit=limit).message_format()
@http.route('/mail/history/messages', methods=['POST'], type='json', auth='user')
def discuss_history_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], max_id=max_id, min_id=min_id, limit=limit).message_format()
def discuss_history_messages(self, before=None, after=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('needaction', '=', False)], before=before, after=after, limit=limit).message_format()
@http.route('/mail/starred/messages', methods=['POST'], type='json', auth='user')
def discuss_starred_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], max_id=max_id, min_id=min_id, limit=limit).message_format()
def discuss_starred_messages(self, before=None, after=None, limit=30, **kwargs):
return request.env['mail.message']._message_fetch(domain=[('starred_partner_ids', 'in', [request.env.user.partner_id.id])], before=before, after=after, limit=limit).message_format()
# --------------------------------------------------------------------------
# Thread API (channel/chatter common)
@@ -406,13 +406,13 @@ class DiscussController(http.Controller):
raise NotFound()
@http.route('/mail/channel/messages', methods=['POST'], type='json', auth='public')
def mail_channel_messages(self, channel_id, max_id=None, min_id=None, limit=30, **kwargs):
def mail_channel_messages(self, channel_id, before=None, after=None, limit=30, **kwargs):
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
messages = channel_member_sudo.env['mail.message']._message_fetch(domain=[
('res_id', '=', channel_id),
('model', '=', 'mail.channel'),
('message_type', '!=', 'user_notification'),
], max_id=max_id, min_id=min_id, limit=limit)
], before=before, after=after, limit=limit)
if not request.env.user._is_public():
messages.set_message_done()
return messages.message_format()
@@ -451,12 +451,12 @@ class DiscussController(http.Controller):
return thread._get_mail_thread_data(request_list)
@http.route('/mail/thread/messages', methods=['POST'], type='json', auth='user')
def mail_thread_messages(self, thread_model, thread_id, max_id=None, min_id=None, limit=30, **kwargs):
def mail_thread_messages(self, thread_model, thread_id, before=None, after=None, limit=30, **kwargs):
messages = request.env['mail.message']._message_fetch(domain=[
('res_id', '=', int(thread_id)),
('model', '=', thread_model),
('message_type', '!=', 'user_notification'),
], max_id=max_id, min_id=min_id, limit=limit)
], before=before, after=after, limit=limit)
if not request.env.user._is_public():
messages.set_message_done()
return messages.message_format()
+1 -3
View File
@@ -732,8 +732,6 @@ class Channel(models.Model):
'message': message_format,
}
notifications.append((channel, 'mail.channel/new_message', payload))
if self.env.user.partner_id == message.author_id:
self._channel_seen(last_message_id=message.id)
return notifications
# ------------------------------------------------------------
@@ -808,7 +806,7 @@ class Channel(models.Model):
if member:
channel_data['channelMembers'] = [('insert', list(member._mail_channel_member_format().values()))]
info['state'] = member.fold_state or 'open'
channel_data['serverMessageUnreadCounter'] = member.message_unread_counter
channel_data['message_unread_counter'] = member.message_unread_counter
info['is_minimized'] = member.is_minimized
info['seen_message_id'] = member.seen_message_id.id
channel_data['custom_channel_name'] = member.custom_channel_name
+7 -7
View File
@@ -917,18 +917,18 @@ class Message(models.Model):
return vals_list
@api.model
def _message_fetch(self, domain, max_id=None, min_id=None, limit=30):
def _message_fetch(self, domain, before=None, after=None, limit=30):
""" Get a limited amount of formatted messages with provided domain.
:param domain: the domain to filter messages;
:param min_id: messages must be more recent than this id
:param max_id: message must be less recent than this id
:param after: messages must be more recent than this id
:param before: message must be less recent than this id
:param limit: the maximum amount of messages to get;
:returns: record set of mail.message
"""
if max_id:
domain = expression.AND([domain, [('id', '<', max_id)]])
if min_id:
domain = expression.AND([domain, [('id', '>', min_id)]])
if before:
domain = expression.AND([domain, [('id', '<', before)]])
if after:
domain = expression.AND([domain, [('id', '>', after)]])
return self.search(domain, limit=limit)
def message_format(self, format_reply=True, msg_vals=None):
@@ -74,9 +74,6 @@ export class Message {
* @returns {boolean}
*/
get editable() {
if (this.isEmpty) {
return false;
}
if (!this._store.user?.isAdmin && !this.isSelfAuthored) {
return false;
}
+7 -40
View File
@@ -125,7 +125,7 @@ export class MessageService {
createTransient(data) {
const { body, res_id, model } = data;
const lastMessageId = this.getLastMessageId();
this.insert({
return this.insert({
author: this.store.odoobot,
body,
id: lastMessageId + 0.01,
@@ -177,17 +177,15 @@ export class MessageService {
updateStarred(message, isStarred) {
message.isStarred = isStarred;
const starred = this.store.discuss.starred;
if (isStarred) {
this.store.discuss.starred.counter++;
if (this.store.discuss.starred.messages.length > 0) {
this.store.discuss.starred.messages.push(message);
starred.counter++;
if (!starred.messages.includes(message)) {
starred.messages.push(message);
}
} else {
this.store.discuss.starred.counter--;
removeFromArrayWithPredicate(
this.store.discuss.starred.messages,
({ id }) => id === message.id
);
starred.counter--;
removeFromArrayWithPredicate(starred.messages, ({ id }) => id === message.id);
}
}
@@ -287,28 +285,6 @@ export class MessageService {
message.originThread.modelName = data.res_model_name;
}
this._updateReactions(message, data.messageReactionGroups);
if (message.originThread && !message.originThread.messages.includes(message)) {
message.originThread.messages.push(message);
this.sortMessages(message.originThread);
}
if (message.isNeedaction && !this.store.discuss.inbox.messages.includes(message)) {
if (!fromFetch) {
this.store.discuss.inbox.counter++;
if (message.originThread) {
message.originThread.message_needaction_counter++;
}
}
this.store.discuss.inbox.messages.push(message);
this.sortMessages(this.store.discuss.inbox);
}
if (message.isStarred && !this.store.discuss.starred.messages.includes(message)) {
this.store.discuss.starred.messages.push(message);
this.sortMessages(this.store.discuss.starred);
}
if (message.isHistory && !this.store.discuss.history.messages.includes(message)) {
this.store.discuss.history.messages.push(message);
this.sortMessages(this.store.discuss.history);
}
}
updateNotifications(message) {
@@ -474,15 +450,6 @@ export class MessageService {
group.resIds.add(data.resId);
}
/**
* @param {import("@mail/core/thread_model").Thread} thread
*/
sortMessages(thread) {
thread.messages.sort((msg1, msg2) => {
return msg1.id - msg2.id;
});
}
scheduledDateSimple(message) {
return message.scheduledDate.toLocaleString(DateTime.TIME_SIMPLE, {
locale: this.userService.lang.replace("_", "-"),
@@ -2,7 +2,6 @@
import { markup, reactive } from "@odoo/owl";
import { Deferred } from "@web/core/utils/concurrency";
import { memoize } from "@web/core/utils/functions";
import { cleanTerm } from "@mail/utils/format";
import { removeFromArray, removeFromArrayWithPredicate } from "@mail/utils/arrays";
import { LinkPreview } from "./link_preview_model";
@@ -251,10 +250,15 @@ export class Messaging {
type: notif.payload.channel.channel_type,
});
break;
case "mail.channel/transient_message":
return this.messageService.createTransient(
case "mail.channel/transient_message": {
const channel =
this.store.threads[createLocalId("mail.channel", notif.payload.res_id)];
const message = this.messageService.createTransient(
Object.assign(notif.payload, { body: markup(notif.payload.body) })
);
channel.messages.push(message);
break;
}
case "mail.link.preview/delete":
{
const { id, message_id } = notif.payload;
@@ -266,7 +270,12 @@ export class Messaging {
break;
case "mail.message/inbox": {
const data = Object.assign(notif.payload, { body: markup(notif.payload.body) });
this.messageService.insert(data);
const message = this.messageService.insert(data);
const inbox = this.store.discuss.inbox;
if (!inbox.messages.includes(message)) {
inbox.messages.push(message);
inbox.counter++;
}
break;
}
case "mail.message/delete": {
@@ -295,12 +304,22 @@ export class Messaging {
message.originThread.messages,
({ id }) => id === message.id
);
if (message.isNeedaction) {
removeFromArrayWithPredicate(
message.originThread.needactionMessages,
({ id }) => id === message.id
);
}
if (message.id > message.originThread.seen_message_id) {
message.originThread.message_unread_counter--;
}
}
}
break;
}
case "mail.message/mark_as_read": {
const { message_ids: messageIds, needaction_inbox_counter } = notif.payload;
const inbox = this.store.discuss.inbox;
for (const messageId of messageIds) {
// We need to ignore all not yet known messages because we don't want them
// to be shown partially as they would be linked directly to cache.
@@ -315,25 +334,25 @@ export class Messaging {
const originThread = message.originThread;
if (originThread && message.isNeedaction) {
originThread.message_needaction_counter--;
removeFromArrayWithPredicate(
originThread.needactionMessages,
({ id }) => id === messageId
);
}
// move messages from Inbox to history
const partnerIndex = message.needaction_partner_ids.find(
(p) => p === this.store.user?.id
);
removeFromArray(message.needaction_partner_ids, partnerIndex);
removeFromArrayWithPredicate(
this.store.discuss.inbox.messages,
({ id }) => id === messageId
);
if (this.store.discuss.history.messages.length > 0) {
this.store.discuss.history.messages.push(message);
removeFromArrayWithPredicate(inbox.messages, ({ id }) => id === messageId);
const history = this.store.discuss.history;
if (!history.messages.includes(message)) {
history.messages.push(message);
}
}
this.store.discuss.inbox.counter = needaction_inbox_counter;
if (
this.store.discuss.inbox.counter > this.store.discuss.inbox.messages.length
) {
this.threadService.fetchMessages(this.store.discuss.inbox);
inbox.counter = needaction_inbox_counter;
if (inbox.counter > inbox.messages.length) {
this.threadService.fetchMoreMessages(inbox);
}
break;
}
@@ -342,7 +361,6 @@ export class Messaging {
for (const messageId of messageIds) {
const message = this.messageService.insert({ id: messageId });
this.messageService.updateStarred(message, starred);
this.messageService.sortMessages(this.store.discuss.starred);
}
break;
}
@@ -354,8 +372,8 @@ export class Messaging {
// knowledge of the channel
continue;
}
if (partner_id && this.store.user?.id === partner_id) {
channel.serverLastSeenMsgBySelf = last_message_id;
if (partner_id && partner_id === this.store.user?.id) {
this.threadService.updateSeen(channel, last_message_id);
}
const seenInfo = channel.seenInfos.find(
(seenInfo) => seenInfo.partner.id === partner_id
@@ -490,6 +508,25 @@ export class Messaging {
res_id: channel.id,
model: channel.model,
});
if (!channel.messages.includes(message)) {
channel.messages.push(message);
if (message.isSelfAuthored) {
channel.seen_message_id = message.id;
} else {
channel.message_unread_counter++;
if (message.isNeedaction) {
const inbox = this.store.discuss.inbox;
if (!inbox.messages.includes(message)) {
inbox.messages.push(message);
inbox.counter++;
}
if (!channel.needactionMessages.includes(message)) {
channel.needactionMessages.push(message);
channel.message_needaction_counter++;
}
}
}
}
if (channel.chatPartnerId !== this.store.odoobot?.id) {
if (!this.presence.isOdooFocused() && channel.isChatChannel) {
this.outOfFocusService.notify(message, channel);
@@ -504,9 +541,9 @@ export class Messaging {
if (
!message.isSelfAuthored &&
channel.composer.isFocused &&
channel.mostRecentNonTransientMessage &&
channel.newestPersistentMessage &&
!this.store.guest &&
channel.mostRecentNonTransientMessage === channel.mostRecentMsg
channel.newestPersistentMessage === channel.newestMessage
) {
this.threadService.markAsRead(channel);
}
@@ -618,25 +655,6 @@ export class Messaging {
// actions that can be performed on the messaging system
// -------------------------------------------------------------------------
fetchPreviews = memoize(async () => {
const ids = [];
for (const thread of Object.values(this.store.threads)) {
if (["channel", "group", "chat"].includes(thread.type)) {
ids.push(thread.id);
}
}
if (ids.length) {
const previews = await this.orm.call("mail.channel", "channel_fetch_preview", [ids]);
for (const preview of previews) {
const thread = this.store.threads[createLocalId("mail.channel", preview.id)];
const data = Object.assign(preview.last_message, {
body: markup(preview.last_message.body),
});
this.messageService.insert({ ...data, res_id: thread.id, model: thread.model });
}
}
});
async searchPartners(searchStr = "", limit = 10) {
let partners = [];
const searchTerm = cleanTerm(searchStr);
+37 -44
View File
@@ -63,12 +63,32 @@ export class Thread {
memberCount = 0;
message_needaction_counter = 0;
message_unread_counter = 0;
/** @type {import("@mail/core/message_model").Message[]} */
/**
* Contains continuous sequence of messages to show in message list.
* There should not be any hole in this list: there can be unknown
* messages before start and after end, but there should not be any
* unknown in-between messages.
*
* Content should be fetched and inserted in a controlled way.
*
* @type {import("@mail/core/message_model").Message[]}
*/
messages = [];
/**
* Contains continuous sequence of needaction messages to show in messaging menu.
* There should not be any hole in this list: there can be unknown
* messages before start and after end, but there should not be any
* unknown in-between messages.
*
* Content should be fetched and inserted in a controlled way.
*
* @type {import("@mail/core/message_model").Message[]}
*/
needactionMessages = [];
/** @type {string} */
name;
/** @type {number|false} */
serverLastSeenMsgBySelf;
seen_message_id;
/** @type {'opened' | 'folded' | 'closed'} */
state;
status = "new";
@@ -82,7 +102,6 @@ export class Thread {
defaultDisplayMode;
/** @type {SeenInfo[]} */
seenInfos = [];
serverMessageUnreadCounter = 0;
/** @type {SuggestedRecipient[]} */
suggestedRecipients = [];
hasLoadingFailed = false;
@@ -241,7 +260,7 @@ export class Thread {
}
get lastEditableMessageOfSelf() {
const editableMessagesBySelf = this.messages.filter(
const editableMessagesBySelf = this.nonEmptyMessages.filter(
(message) => message.isSelfAuthored && message.editable
);
if (editableMessagesBySelf.length > 0) {
@@ -254,41 +273,25 @@ export class Thread {
return createLocalId(this.model, this.id);
}
get needactionMessages() {
return this.messages.filter(({ isNeedaction }) => isNeedaction);
}
/** @returns {import("@mail/core/message_model").Message | undefined} */
get mostRecentMsg() {
if (this.messages.length === 0) {
return undefined;
}
return this._store.messages[Math.max(...this.nonEmptyMessages.map((m) => m.id))];
get newestMessage() {
return [...this.messages].reverse().find((msg) => !msg.isEmpty);
}
get mostRecentNeedactionMsg() {
const mostRecentNeedactionMsgId = this.mostRecentNeedactionMsgId;
if (!mostRecentNeedactionMsgId) {
return undefined;
}
return this._store.messages[mostRecentNeedactionMsgId];
get newestNeedactionMessage() {
return this.needactionMessages[this.needactionMessages.length - 1];
}
get mostRecentNeedactionMsgId() {
const needactionMessages = this.needactionMessages;
return needactionMessages.length > 0
? Math.max(...needactionMessages.map(({ id }) => id))
: undefined;
get oldestNeedactionMessage() {
return this.needactionMessages[0];
}
get mostRecentNonTransientMessage() {
if (this.messages.length === 0) {
return undefined;
}
const oldestNonTransientMessage = [...this.messages]
.reverse()
.find((message) => Number.isInteger(message.id));
return oldestNonTransientMessage;
get newestPersistentMessage() {
return [...this.messages].reverse().find((msg) => Number.isInteger(msg.id));
}
get oldestPersistentMessage() {
return this.messages.find((msg) => Number.isInteger(msg.id));
}
get hasSelfAsMember() {
@@ -318,21 +321,11 @@ export class Thread {
return orderedOnlineMembers.sort((m1, m2) => (m1.persona.name < m2.persona.name ? -1 : 1));
}
get oldestNonTransientMessage() {
if (this.messages.length === 0) {
return undefined;
}
const oldestNonTransientMessage = this.messages.find((message) =>
Number.isInteger(message.id)
);
return oldestNonTransientMessage;
}
get nonEmptyMessages() {
return this.messages.filter((message) => !message.isEmpty);
}
get nonTransientMessages() {
get persistentMessages() {
return this.messages.filter((message) => !message.isTransient);
}
@@ -350,7 +343,7 @@ export class Thread {
return false;
}
const lastMessageSeenByAllId = Math.min(...otherLastSeenMessageIds);
const orderedSelfSeenMessages = this.nonTransientMessages.filter((message) => {
const orderedSelfSeenMessages = this.persistentMessages.filter((message) => {
return message.author === this._store.self && message.id <= lastMessageSeenByAllId;
});
if (!orderedSelfSeenMessages || orderedSelfSeenMessages.length === 0) {
+138 -78
View File
@@ -13,11 +13,12 @@ import { Composer } from "../composer/composer_model";
import { prettifyMessageContent } from "../utils/format";
import { registry } from "@web/core/registry";
import { url } from "@web/core/utils/urls";
import { memoize } from "@web/core/utils/functions";
import { DEFAULT_AVATAR } from "@mail/core/persona_service";
import { loadEmoji } from "@mail/emoji_picker/emoji_picker";
import { browser } from "@web/core/browser/browser";
const FETCH_MSG_LIMIT = 30;
const FETCH_LIMIT = 30;
export class ThreadService {
nextId = 0;
@@ -104,18 +105,16 @@ export class ThreadService {
async markAsRead(thread) {
if (!thread.isLoaded && thread.status === "loading") {
await thread.isLoadedDeferred;
await new Promise(setTimeout);
}
const mostRecentNonTransientMessage = thread.mostRecentNonTransientMessage;
if (
this.isUnread(thread) &&
thread.allowSetLastSeenMessage &&
mostRecentNonTransientMessage
) {
const newestPersistentMessage = thread.newestPersistentMessage;
thread.seen_message_id = thread.newestPersistentMessage?.id ?? false;
if (this.isUnread(thread) && thread.allowSetLastSeenMessage && newestPersistentMessage) {
this.rpc("/mail/channel/set_last_seen_message", {
channel_id: thread.id,
last_message_id: mostRecentNonTransientMessage.id,
last_message_id: newestPersistentMessage.id,
}).then(() => {
this.update(thread, { serverLastSeenMsgBySelf: mostRecentNonTransientMessage.id });
this.updateSeen(thread, newestPersistentMessage.id);
});
}
if (thread.hasNeedactionMessages) {
@@ -123,13 +122,38 @@ export class ThreadService {
}
}
markAllMessagesAsRead(thread) {
return this.orm.silent.call("mail.message", "mark_all_as_read", [
updateSeen(thread, lastSeenId = thread.newestPersistentMessage?.id) {
const lastReadIndex = thread.messages.findIndex((message) => message.id === lastSeenId);
let newNeedactionCounter = 0;
let newUnreadCounter = 0;
for (const message of thread.messages.slice(lastReadIndex + 1)) {
if (message.isNeedaction) {
newNeedactionCounter++;
}
if (Number.isInteger(message.id)) {
newUnreadCounter++;
}
}
this.update(thread, {
seen_message_id: lastSeenId,
message_needaction_counter: newNeedactionCounter,
message_unread_counter: newUnreadCounter,
});
}
async markAllMessagesAsRead(thread) {
await this.orm.silent.call("mail.message", "mark_all_as_read", [
[
["model", "=", thread.model],
["res_id", "=", thread.id],
],
]);
Object.assign(thread, {
needactionMessages: [],
message_unread_counter: 0,
message_needaction_counter: 0,
seen_message_id: thread.newestPersistentMessage?.id,
});
}
/**
@@ -141,9 +165,9 @@ export class ThreadService {
/**
* @param {Thread} thread
* @param {{min: Number, max: Number}}
* @param {{after: Number, before: Number}}
*/
async fetchMessages(thread, { min, max } = {}) {
async fetchMessages(thread, { after, before } = {}) {
thread.status = "loading";
if (thread.type === "chatter" && !thread.id) {
return [];
@@ -174,11 +198,12 @@ export class ThreadService {
return {};
})();
try {
// ordered messages received: newest to oldest
const rawMessages = await this.rpc(route, {
...params,
limit: FETCH_MSG_LIMIT,
max_id: max,
min_id: min,
limit: FETCH_LIMIT,
after,
before,
});
const messages = rawMessages.reverse().map((data) => {
if (data.parentMessage) {
@@ -205,17 +230,60 @@ export class ThreadService {
* @param {Thread} thread
*/
async fetchNewMessages(thread) {
if (thread.status === "loading" || (thread.isLoaded && thread.model === "mail.channel")) {
if (
thread.status === "loading" ||
(thread.isLoaded && ["mail.channel", "mail.box"].includes(thread.model))
) {
return;
}
const min = thread.isLoaded ? thread.mostRecentNonTransientMessage?.id : undefined;
const after = thread.isLoaded ? thread.newestPersistentMessage?.id : undefined;
try {
const fetchedMsgs = await this.fetchMessages(thread, { min });
const fetched = await this.fetchMessages(thread, { after });
// feed messages
// could have received a new message as notification during fetch
// filter out already fetched (e.g. received as notification in the meantime)
const startIndex =
after === undefined
? 0
: thread.messages.findIndex((message) => message.id === after);
const alreadyKnownMessages = new Set(thread.messages.map((m) => m.id));
const filtered = fetched.filter(
(message) =>
!alreadyKnownMessages.has(message.id) &&
(thread.persistentMessages.length === 0 ||
message.id < thread.oldestPersistentMessage.id ||
message.id > thread.newestPersistentMessage.id)
);
thread.messages.splice(startIndex, 0, ...filtered);
// feed needactions
// same for needaction messages, special case for mailbox:
// kinda "fetch new/more" with needactions on many origin threads at once
if (thread === this.store.discuss.inbox) {
for (const message of fetched) {
const thread = message.originThread;
if (!thread.needactionMessages.includes(message)) {
thread.needactionMessages.unshift(message);
}
}
} else {
const startNeedactionIndex =
after === undefined
? 0
: thread.messages.findIndex((message) => message.id === after);
const filteredNeedaction = fetched.filter(
(message) =>
message.isNeedaction &&
(thread.needactionMessages.length === 0 ||
message.id < thread.oldestNeedactionMessage.id ||
message.id > thread.newestNeedactionMessage.id)
);
thread.needactionMessages.splice(startNeedactionIndex, 0, ...filteredNeedaction);
}
Object.assign(thread, {
loadMore:
min === undefined && fetchedMsgs.length === FETCH_MSG_LIMIT
after === undefined && fetched.length === FETCH_LIMIT
? true
: min === undefined && fetchedMsgs.length !== FETCH_MSG_LIMIT
: after === undefined && fetched.length !== FETCH_LIMIT
? false
: thread.loadMore,
});
@@ -224,6 +292,39 @@ export class ThreadService {
}
}
// This function is like fetchNewMessages but just for a single message at most on all pinned threads
fetchPreviews = memoize(async () => {
const ids = [];
for (const thread of Object.values(this.store.threads)) {
if (["channel", "group", "chat"].includes(thread.type)) {
ids.push(thread.id);
}
}
if (ids.length) {
const previews = await this.orm.call("mail.channel", "channel_fetch_preview", [ids]);
for (const preview of previews) {
const thread = this.store.threads[createLocalId("mail.channel", preview.id)];
const data = Object.assign(preview.last_message, {
body: markup(preview.last_message.body),
});
const message = this.messageService.insert({
...data,
res_id: thread.id,
model: thread.model,
});
if (!thread.isLoaded) {
thread.messages.push(message);
if (message.isNeedaction && !thread.needactionMessages.includes(message)) {
thread.needactionMessages.push(message);
}
}
thread.isLoaded = true;
thread.loadMore = true;
thread.status = "ready";
}
}
});
/**
* @param {Thread} thread
*/
@@ -232,10 +333,11 @@ export class ThreadService {
return;
}
try {
const fetchedMsgs = await this.fetchMessages(thread, {
max: thread.oldestNonTransientMessage?.id,
const fetched = await this.fetchMessages(thread, {
before: thread.oldestPersistentMessage?.id,
});
if (fetchedMsgs.length < FETCH_MSG_LIMIT) {
thread.messages.unshift(...fetched);
if (fetched.length < FETCH_LIMIT) {
thread.loadMore = false;
}
} catch {
@@ -484,23 +586,21 @@ export class ThreadService {
"is_pinned",
"message_needaction_counter",
"name",
"seen_message_id",
"state",
"group_based_subscription",
"last_interest_dt",
"defaultDisplayMode",
]);
if (serverData.channel && "message_unread_counter" in serverData.channel) {
thread.message_unread_counter = serverData.channel.message_unread_counter;
}
thread.lastServerMessageId = serverData.last_message_id ?? thread.lastServerMessageId;
if (thread.model === "mail.channel" && serverData.channel) {
thread.channel = assignDefined(thread.channel ?? {}, serverData.channel);
}
thread.memberCount = serverData.channel?.memberCount ?? thread.memberCount;
if (serverData.channel && "serverMessageUnreadCounter" in serverData.channel) {
thread.serverMessageUnreadCounter = serverData.channel.serverMessageUnreadCounter;
}
if ("seen_message_id" in serverData) {
thread.serverLastSeenMsgBySelf = serverData.seen_message_id;
}
if ("rtc_inviting_session" in serverData) {
this.env.bus.trigger("THREAD-SERVICE:UPDATE_RTC_SESSIONS", {
thread,
@@ -744,6 +844,8 @@ export class ThreadService {
model: thread.model,
temporary_id: tmpId,
});
thread.messages.push(tmpMsg);
thread.seen_message_id = tmpMsg.id;
}
const data = await this.rpc("/mail/message/post", params);
if (data.parentMessage) {
@@ -757,6 +859,9 @@ export class ThreadService {
const message = this.messageService.insert(
Object.assign(data, { body: markup(data.body) })
);
if (!thread.messages.some(({ id }) => id === message.id)) {
thread.messages.push(message);
}
if (!message.isEmpty && this.store.hasLinkPreviewFeature) {
this.rpc("/mail/link_preview", { message_id: data.id }, { silent: true });
}
@@ -771,7 +876,7 @@ export class ThreadService {
* @param {Thread} thread
*/
isUnread(thread) {
return this.localMessageUnreadCounter(thread) > 0;
return thread.message_unread_counter > 0;
}
/**
@@ -800,63 +905,18 @@ export class ThreadService {
return thread.counter;
}
if (thread.type === "chat" || thread.type === "group") {
return this.localMessageUnreadCounter(thread);
return thread.message_unread_counter || thread.message_needaction_counter;
}
return thread.message_needaction_counter;
}
/**
* @param {Thread} thread
*/
localMessageUnreadCounter(thread) {
let baseCounter = thread.serverMessageUnreadCounter;
let countFromId = thread.lastServerMessageId ? thread.lastServerMessageId : 0;
const lastSeenMessageId = this.lastSeenBySelfMessageId(thread);
const firstMessage = thread.messages[0];
if (firstMessage && (lastSeenMessageId === false || lastSeenMessageId >= firstMessage.id)) {
baseCounter = 0;
countFromId = lastSeenMessageId || 0;
}
return thread.messages.reduce((total, message) => {
if (message.id <= countFromId || message.temporary_id) {
return total;
}
return total + 1;
}, baseCounter);
}
/**
* @param {Thread} thread
*/
lastSeenBySelfMessageId(thread) {
if (thread.model !== "mail.channel") {
return null;
}
const firstMessage = thread.messages[0];
if (firstMessage && thread.serverLastSeenMsgBySelf < firstMessage.id) {
return thread.serverLastSeenMsgBySelf;
}
let lastSeenMessageId = thread.serverLastSeenMsgBySelf;
for (const message of thread.messages) {
if (message.id <= thread.serverLastSeenMsgBySelf) {
continue;
}
if (message.temporary_id || message.isTransient) {
lastSeenMessageId = message.id;
continue;
}
return lastSeenMessageId;
}
return lastSeenMessageId;
}
getDiscussCategoryCounter(categoryId) {
return this.store.discuss[categoryId].threads.reduce((acc, threadLocalId) => {
const channel = this.store.threads[threadLocalId];
if (categoryId === "channels") {
return channel.message_needaction_counter > 0 ? acc + 1 : acc;
} else {
return this.localMessageUnreadCounter(channel) > 0 ? acc + 1 : acc;
return this.isUnread(channel) > 0 ? acc + 1 : acc;
}
}, 0);
}
+1 -1
View File
@@ -336,7 +336,7 @@ export class Message extends Component {
onClickMarkAsUnread() {
const previousMessageId =
this.message.originThread.getPreviousMessage(this.message)?.id ?? false;
if (this.threadService.lastSeenBySelfMessageId(this.props.thread) === previousMessageId) {
if (this.props.thread.seen_message_id === previousMessageId) {
return;
}
return this.rpc("/mail/channel/set_last_seen_message", {
+1 -1
View File
@@ -145,7 +145,7 @@
<button t-if="canAddReaction" class="btn px-1 py-0" tabindex="1" title="Add a Reaction" aria-label="Add a Reaction" t-ref="emoji-picker"><i class="fa fa-lg fa-smile-o"/></button>
<button t-if="canReplyTo" class="btn px-1 py-0" t-on-click.stop="onClickReplyTo" tabindex="1" title="Reply" aria-label="Reply"><i class="fa fa-lg fa-reply"/></button>
<button t-if="canToggleStar" class="btn px-1 py-0" t-on-click="() => messageService.toggleStar(props.message)" tabindex="0" title="Mark as Todo" aria-label="Mark as Todo"><i class="fa fa-lg" t-att-class="message.isStarred ? 'fa-star o-mail-Message-starred' : 'fa-star-o'"/></button>
<Dropdown t-if="editable or deletable or isInInbox or props.thread.model === 'mail.channel'" onStateChanged="state => this.state.expandOptions = state.open" position="props.thread?.mostRecentMsg === props.message ? 'top-start' : 'bottom-start'" togglerClass="`btn p-0 ${ state.expandOptions ? 'bg-200' : '' }`" menuClass="'d-flex flex-column py-0 o-mail-Message-moreMenu'" class="'d-flex'" title="expandText">
<Dropdown t-if="editable or deletable or isInInbox or props.thread.model === 'mail.channel'" onStateChanged="state => this.state.expandOptions = state.open" position="props.thread?.newestMessage === props.message ? 'top-start' : 'bottom-start'" togglerClass="`btn p-0 ${ state.expandOptions ? 'bg-200' : '' }`" menuClass="'d-flex flex-column py-0 o-mail-Message-moreMenu'" class="'d-flex'" title="expandText">
<t t-set-slot="toggler">
<i class="btn px-1 py-0 fa fa-lg fa-ellipsis-h" t-att-class="{ 'order-1': props.isInChatWindow }" tabindex="1"/>
</t>
+5 -6
View File
@@ -62,7 +62,7 @@ export class Thread extends Component {
this.threadService.fetchMoreMessages(this.props.thread);
}
});
this.oldestNonTransientMessageId = null;
this.oldestPersistentMessageId = null;
this.scrollPosition = useScrollPosition(
"messages",
this.props.thread.scrollPosition,
@@ -73,8 +73,8 @@ export class Thread extends Component {
onWillPatch: () => {
return {
hasMoreMsgsAbove:
this.props.thread.oldestNonTransientMessage?.id !==
this.oldestNonTransientMessage && this.props.order === "asc",
this.props.thread.oldestPersistentMessage?.id !==
this.oldestPersistentMessage && this.props.order === "asc",
};
},
onPatched: ({ hasMoreMsgsAbove, scrollTop, scrollHeight }) => {
@@ -82,13 +82,12 @@ export class Thread extends Component {
if (hasMoreMsgsAbove) {
el.scrollTop = scrollTop + el.scrollHeight - scrollHeight;
}
this.oldestNonTransientMessage =
this.props.thread.oldestNonTransientMessage?.id;
this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id;
},
});
}
onMounted(() => {
this.oldestNonTransientMessage = this.props.thread.oldestNonTransientMessage?.id;
this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id;
if (!this.env.inChatter || this.props.hasMessageScrollAdjustInChatter) {
this.scrollPosition.restore();
}
+1 -1
View File
@@ -20,7 +20,7 @@
</div>
<t t-set="currentDay" t-value="msg.dateDay"/>
</t>
<Transition visible="threadService.lastSeenBySelfMessageId(props.thread) === (prevMsg?.id ?? false)" name="'o-fade'" t-slot-scope="transition">
<Transition visible="props.thread.seen_message_id === (prevMsg?.id ?? false)" name="'o-fade'" t-slot-scope="transition">
<div class="o-mail-Thread-newMessage d-flex align-items-center fw-bolder" t-att-class="{ 'opacity-0': transition.className.includes('o-fade-leave') }">
<hr class="flex-grow-1 border opacity-50"/><span class="px-3">New messages</span>
</div>
@@ -21,8 +21,8 @@
'ms-3 me-1 my-0': !thread,
}"
/>
<div t-if="thread and thread.isChannel and threadService.localMessageUnreadCounter(thread) > 0" class="mx-1 my-0">
(<t t-out="threadService.localMessageUnreadCounter(thread)"/>)
<div t-if="thread and thread.isChannel and threadService.unreadCounter(thread) > 0" class="mx-1 my-0">
(<t t-out="threadService.unreadCounter(thread)"/>)
</div>
<div class="flex-grow-1"/>
<div t-if="thread and thread.allowCalls and thread !== rtc.state.channel and !props.chatWindow.hidden" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" title="Start a Call" t-on-click.stop="() => this.rtc.toggleCall(this.props.chatWindow.thread)">
@@ -42,13 +42,13 @@ export class MessagingMenu extends Component {
}
beforeOpen() {
this.messaging.fetchPreviews();
this.threadService.fetchPreviews();
if (
!this.store.discuss.inbox.isLoaded &&
this.store.discuss.inbox.status !== "loading" &&
this.store.discuss.inbox.counter !== this.store.discuss.inbox.messages.length
) {
this.threadService.fetchMessages(this.store.discuss.inbox);
this.threadService.fetchNewMessages(this.store.discuss.inbox);
}
}
@@ -117,24 +117,22 @@ export class MessagingMenu extends Component {
if (a.correspondent === this.store.odoobot) {
return 1;
}
if (!a.mostRecentNonTransientMessage?.datetime) {
if (!a.newestPersistentMessage?.datetime) {
return -1;
}
if (!b.mostRecentNonTransientMessage?.datetime) {
if (!b.newestPersistentMessage?.datetime) {
return 1;
}
return (
b.mostRecentNonTransientMessage.datetime - a.mostRecentNonTransientMessage.datetime
);
return b.newestPersistentMessage.datetime - a.newestPersistentMessage.datetime;
});
const previews = [];
for (const thread of threads) {
const { mostRecentMsg, mostRecentNeedactionMsg } = thread;
const { newestMessage, newestNeedactionMessage } = thread;
if (thread.is_pinned) {
const message = mostRecentMsg;
const message = newestMessage;
previews.push({
id: `preview-${thread.localId}`,
count: this.threadService.localMessageUnreadCounter(thread),
count: thread.message_unread_counter,
imgUrl: thread.imgUrl,
hasMarkAsReadButton: this.threadService.isUnread(thread),
message,
@@ -142,8 +140,8 @@ export class MessagingMenu extends Component {
isNeedaction: false,
});
}
if (mostRecentNeedactionMsg) {
const message = mostRecentNeedactionMsg;
if (newestNeedactionMessage) {
const message = newestNeedactionMessage;
previews.push({
id: `preview-needaction-${thread.localId}`,
count: thread.needactionMessages.length,
@@ -116,7 +116,8 @@ patch(ThreadService.prototype, "mail/web", {
res_id: thread.id,
model: thread.model,
};
this.messageService.insert(tmpData);
const message = this.messageService.insert(tmpData);
thread.messages.push(message);
}
return thread;
},
@@ -7,8 +7,6 @@ QUnit.module("message model test", {});
QUnit.test("Message model properties", async (assert) => {
const { env } = await start();
/** @type {import("@mail/core/store_service").Store} */
const store = env.services["mail.store"];
env.services["mail.thread"].insert({
id: 3,
model: "res.partner",
@@ -42,8 +40,6 @@ QUnit.test("Message model properties", async (assert) => {
"2019-05-05 10:00:00"
);
assert.strictEqual(message.id, 4000);
assert.ok(store.discuss.inbox.messages.find((m) => m.id === message.id));
assert.ok(store.discuss.starred.messages.find((m) => m.id === message.id));
assert.ok(message.attachments);
assert.strictEqual(message.attachments[0].name, "test.txt");
@@ -1557,7 +1557,6 @@ QUnit.test("new messages separator [REQUIRE FOCUS]", async (assert) => {
// this test requires several messages so that the last message is not
// visible. This is necessary in order to display 'new messages' and not
// remove from DOM right away from seeing last message.
// AKU TODO: thread specific test
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Foreigner partner" });
const userId = pyEnv["res.users"].create({
@@ -1155,7 +1155,7 @@ QUnit.test("Group unread counter up to date after mention is marked as seen", as
author_id: partnerId,
model: "mail.channel",
res_id: channelId,
body: `@Mitchell Admin`,
body: "@Mitchell Admin",
needaction: true,
});
pyEnv["mail.notification"].create([
@@ -40,8 +40,8 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
return this._mockRouteMailAttachmentRemove(attachment_id);
}
if (route === "/mail/channel/messages") {
const { channel_id, min_id, max_id, limit } = args;
return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit);
const { channel_id, after, before, limit } = args;
return this._mockRouteMailChannelMessages(channel_id, before, after, limit);
}
if (route === "/mail/channel/notify_typing") {
const id = args.channel_id;
@@ -60,15 +60,15 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
return this._mockMailChannelLoadMoreMembers([channel_id], known_member_ids);
}
if (route === "/mail/history/messages") {
const { min_id, max_id, limit } = args;
return this._mockRouteMailMessageHistory(min_id, max_id, limit);
const { after, before, limit } = args;
return this._mockRouteMailMessageHistory(after, before, limit);
}
if (route === "/mail/init_messaging") {
return this._mockRouteMailInitMessaging();
}
if (route === "/mail/inbox/messages") {
const { min_id, max_id, limit } = args;
return this._mockRouteMailMessageInbox(min_id, max_id, limit);
const { after, before, limit } = args;
return this._mockRouteMailMessageInbox(after, before, limit);
}
if (route === "/mail/link_preview") {
return this._mockRouteMailLinkPreview(args.message_id);
@@ -142,8 +142,8 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
return;
}
if (route === "/mail/starred/messages") {
const { min_id, max_id, limit } = args;
return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit);
const { after, before, limit } = args;
return this._mockRouteMailMessageStarredMessages(after, before, limit);
}
if (route === "/mail/thread/data") {
return this._mockRouteMailThreadData(
@@ -153,12 +153,12 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
);
}
if (route === "/mail/thread/messages") {
const { min_id, max_id, limit, thread_model, thread_id } = args;
const { after, before, limit, thread_model, thread_id } = args;
return this._mockRouteMailThreadFetchMessages(
thread_model,
thread_id,
max_id,
min_id,
before,
after,
limit
);
}
@@ -191,17 +191,17 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
* @private
* @param {integer} channel_id
* @param {integer} limit
* @param {integer} max_id
* @param {integer} min_id
* @param {integer} before
* @param {integer} after
* @returns {Object} list of messages
*/
async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) {
async _mockRouteMailChannelMessages(channel_id, before = false, after = false, limit = 30) {
const domain = [
["res_id", "=", channel_id],
["model", "=", "mail.channel"],
["message_type", "!=", "user_notification"],
];
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit);
this._mockMailMessageSetMessageDone(messages.map((message) => message.id));
return this._mockMailMessageMessageFormat(messages.map((message) => message.id));
},
@@ -297,9 +297,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
* @private
* @returns {Object}
*/
_mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) {
_mockRouteMailMessageHistory(after = false, before = false, limit = 30) {
const domain = [["needaction", "=", false]];
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit);
const messagesWithNotification = messages.filter((message) => {
const notifs = this.pyEnv["mail.notification"].searchRead([
["mail_message_id", "=", message.id],
@@ -319,9 +319,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
* @private
* @returns {Object}
*/
_mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) {
_mockRouteMailMessageInbox(after = false, before = false, limit = 30) {
const domain = [["needaction", "=", true]];
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit);
return this._mockMailMessageMessageFormat(messages.map((message) => message.id));
},
/**
@@ -330,9 +330,9 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
* @private
* @returns {Object}
*/
_mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) {
_mockRouteMailMessageStarredMessages(after = false, before = false, limit = 30) {
const domain = [["starred_partner_ids", "in", [this.currentPartnerId]]];
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit);
return this._mockMailMessageMessageFormat(messages.map((message) => message.id));
},
/**
@@ -514,16 +514,16 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
* @private
* @param {string} res_model
* @param {integer} res_id
* @param {integer} max_id
* @param {integer} min_id
* @param {integer} before
* @param {integer} after
* @param {integer} limit
* @returns {Object[]} list of messages
*/
async _mockRouteMailThreadFetchMessages(
res_model,
res_id,
max_id = false,
min_id = false,
before = false,
after = false,
limit = 30
) {
const domain = [
@@ -531,7 +531,7 @@ patch(MockServer.prototype, "mail/controllers/discuss", {
["model", "=", res_model],
["message_type", "!=", "user_notification"],
];
const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit);
const messages = this._mockMailMessage_MessageFetch(domain, before, after, limit);
this._mockMailMessageSetMessageDone(messages.map((message) => message.id));
return this._mockMailMessageMessageFormat(messages.map((message) => message.id));
},
@@ -552,7 +552,7 @@ patch(MockServer.prototype, "mail/models/mail_channel", {
});
Object.assign(channelData, {
custom_channel_name: memberOfCurrentUser.custom_channel_name,
serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter,
message_unread_counter: memberOfCurrentUser.message_unread_counter,
});
if (memberOfCurrentUser.rtc_inviting_session_id) {
res["rtc_inviting_session"] = {
@@ -134,17 +134,17 @@ patch(MockServer.prototype, "mail/models/mail_message", {
*
* @private
* @param {Array[]} domain
* @param {integer} [max_id]
* @param {integer} [min_id]
* @param {integer} [before]
* @param {integer} [after]
* @param {integer} [limit=30]
* @returns {Object[]}
*/
_mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) {
if (max_id) {
domain.push(["id", "<", max_id]);
_mockMailMessage_MessageFetch(domain, before, after, limit = 30) {
if (before) {
domain.push(["id", "<", before]);
}
if (min_id) {
domain.push(["id", ">", min_id]);
if (after) {
domain.push(["id", ">", after]);
}
const messages = this.getRecords("mail.message", domain);
// sorted from highest ID to lowest ID (i.e. from youngest to oldest)
@@ -174,6 +174,11 @@ function getOpenDiscuss(webClient, { context = {}, params = {}, ...props } = {})
);
if (waitUntilMessagesLoaded) {
const messagesLoadedPromise = makeDeferred();
const store = webClient.env.services["mail.store"];
const thread = store.threads[store.discuss.threadLocalId];
if (thread.isLoaded) {
messagesLoadedPromise.resolve();
}
let loadMessageRoute = `/mail/${threadId}/messages`;
if (Number.isInteger(threadId)) {
loadMessageRoute = "/mail/channel/messages";
@@ -1421,12 +1421,22 @@ QUnit.test("Can reply to chatter messages from history", async (assert) => {
QUnit.test("Mark as unread", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ channel_type: "chat", name: "General" });
pyEnv["mail.message"].create({
const channelId = pyEnv["mail.channel"].create({
channel_type: "chat",
name: "General",
});
const messageId = pyEnv["mail.message"].create({
model: "mail.channel",
res_id: channelId,
body: "Hello World!",
});
const [memberId] = pyEnv["mail.channel.member"].search([
["channel_id", "=", channelId],
["partner_id", "=", pyEnv.currentPartnerId],
]);
pyEnv["mail.channel.member"].write([memberId], {
seen_message_id: messageId,
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await click("[title='Expand']");
@@ -837,7 +837,12 @@ QUnit.test(
async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Partner1" });
const channelId = pyEnv["mail.channel"].create({ name: "Test" });
const channelId = pyEnv["mail.channel"].create({
name: "Test",
channel_member_ids: [
[0, 0, { message_unread_counter: 2, partner_id: pyEnv.currentPartnerId }],
],
});
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
body: "Message with needaction",
@@ -880,6 +880,7 @@ QUnit.test(
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ name: "General" });
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
const { env, openDiscuss } = await start({
mockRPC(route, args) {
if (args.model === "mail.message" && args.method === "mark_all_as_read") {
@@ -896,6 +897,7 @@ QUnit.test(
await triggerEvents($(".o-mail-Composer-input")[0], null, ["blur", "focusout"]);
await click("button:contains(Inbox)");
const messageId = pyEnv["mail.message"].create({
author_id: partnerId,
body: "@Mitchel Admin",
needaction: true,
model: "mail.channel",
@@ -142,7 +142,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_general.id,
'memberCount': len(self.group_user.users | self.user_root),
'serverMessageUnreadCounter': 5,
'message_unread_counter': 5,
},
'create_uid': self.user_root.id,
'defaultDisplayMode': False,
@@ -191,7 +191,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_channel_public_1.id,
'memberCount': 5,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -240,7 +240,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_channel_public_2.id,
'memberCount': 5,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -289,7 +289,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_channel_group_1.id,
'memberCount': 5,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -338,7 +338,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_channel_group_2.id,
'memberCount': 5,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -409,7 +409,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_group_1.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -494,7 +494,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_chat_1.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -579,7 +579,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_chat_2.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -664,7 +664,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_chat_3.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -749,7 +749,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_chat_4.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -828,7 +828,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_livechat_1.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.user.id,
'defaultDisplayMode': False,
@@ -907,7 +907,7 @@ class TestDiscussFullPerformance(TransactionCase):
'custom_channel_name': False,
'id': self.channel_livechat_2.id,
'memberCount': 2,
'serverMessageUnreadCounter': 0,
'message_unread_counter': 0,
},
'create_uid': self.env.ref('base.public_user').id,
'defaultDisplayMode': False,