[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:
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user