[FIX] mail: only display followers with mail.mt_comment activated

Before this commit, recipient list in chatter when using composer
in "Send message" mode where listing emails of all followers, instead
of only subscribers of "Send message" (= `mail.mt_comment`).

This commit solves this issue by loading and showing followers
that are recipients in this list. Because this feature and follower
list lazy-load some data, they need to manage they own list for
the "load-more" aspect. That's why the diff shows some duplicated
code as with `followers` field of JS thread model.

closes odoo/odoo#134225

X-original-commit: ba43502b22f8f00e7a2318b341302b809e7129dc
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Didier (did)
2023-09-05 21:18:14 +00:00
parent e9266ad1c0
commit a6ebc20b73
15 changed files with 100 additions and 43 deletions
+15 -1
View File
@@ -3966,12 +3966,18 @@ class MailThread(models.AbstractModel):
return True
def message_get_followers(self, after=None, limit=100):
def message_get_followers(self, after=None, limit=100, filter_recipients=False):
self.ensure_one()
domain = [
("res_id", "=", self.id),
("res_model", "=", self._name),
]
if filter_recipients:
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
domain = expression.AND([domain, [
('subtype_ids', '=', subtype_id),
('partner_id', '!=', self.env.user.partner_id.id),
]])
if after:
domain = expression.AND([domain, [('id', '>', after)]])
return self.env["mail.followers"].search(domain, limit=limit, order='id ASC')._format_for_chatter()
@@ -4145,6 +4151,14 @@ class MailThread(models.AbstractModel):
])._format_for_chatter()
res['selfFollower'] = self_follower[0] if len(self_follower) > 0 else None
res['followers'] = self.message_get_followers()
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
res['recipientsCount'] = self.env['mail.followers'].search_count([
("res_id", "=", self.id),
("res_model", "=", self._name),
('partner_id', '!=', self.env.user.partner_id.id),
('subtype_ids', '=', subtype_id),
])
res['recipients'] = self.message_get_followers(filter_recipients=True)
if 'suggestedRecipients' in request_list:
res['suggestedRecipients'] = self._message_get_suggested_recipients()[self.id]
return res
+1
View File
@@ -258,6 +258,7 @@ class Users(models.Model):
'initBusId': self.env['bus.bus'].sudo()._bus_last_id(),
'internalUserGroupId': self.env.ref('base.group_user').id,
'menu_id': self.env['ir.model.data']._xmlid_to_res_id('mail.menu_root_discuss'),
'mt_comment_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'),
'needaction_inbox_counter': self.partner_id._get_needaction_count(),
'odoobot': odoobot.sudo().mail_partner_format().get(odoobot),
'shortcodes': self.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution']),
@@ -87,6 +87,7 @@ export class Messaging {
this.store.discuss.inbox.counter = data.needaction_inbox_counter;
this.store.internalUserGroupId = data.internalUserGroupId;
this.store.discuss.starred.counter = data.starred_counter;
this.store.mt_comment_id = data.mt_comment_id;
this.store.discuss.isActive =
data.menu_id === this.router.current.hash?.menu_id ||
this.router.hash?.action === "mail.action_discuss";
@@ -76,6 +76,13 @@ export class Thread extends Record {
return thread;
}
setup() {}
constructor() {
super();
this.setup();
}
/** @type {number} */
id;
/** @type {string} */
+6 -14
View File
@@ -30,7 +30,7 @@ import { Dropdown } from "@web/core/dropdown/dropdown";
import { _t } from "@web/core/l10n/translation";
import { usePopover } from "@web/core/popover/popover_hook";
import { useService } from "@web/core/utils/hooks";
import { escape } from "@web/core/utils/strings";
import { escapeHTML } from "@web/core/utils/strings";
import { useThrottleForAnimation } from "@web/core/utils/timing";
import { FileUploader } from "@web/views/fields/file_handler";
@@ -220,17 +220,9 @@ export class Chatter extends Component {
* @returns {string}
*/
get toRecipientsText() {
const allFollowers = [];
if (this.state.thread.selfFollower) {
allFollowers.push(this.state.thread.selfFollower);
}
allFollowers.push(...this.state.thread.followers);
const followers = allFollowers.slice(0, 5).map(({ partner }) => {
if (partner.eq(this.store.self)) {
return `<span class="text-muted" title="${escape(partner.email)}">me</span>`;
}
const recipients = [...this.state.thread.recipients].slice(0, 5).map(({ partner }) => {
const text = partner.email ? partner.emailWithoutDomain : partner.name;
return `<span class="text-muted" title="${escape(partner.email)}">${escape(
return `<span class="text-muted" title="${escapeHTML(partner.email)}">${escapeHTML(
text
)}</span>`;
});
@@ -238,10 +230,10 @@ export class Chatter extends Component {
this.store.env.services["user"].lang?.replace("_", "-"),
{ type: "unit" }
);
if (allFollowers.length > 5) {
followers.push("…");
if (this.state.thread.recipients.size > 5) {
recipients.push("…");
}
return markup(formatter.format(followers));
return markup(formatter.format(recipients));
}
/**
@@ -27,6 +27,7 @@ export class FollowerSubtypeDialog extends Component {
setup() {
this.rpc = useService("rpc");
this.store = useState(useService("mail.store"));
this.state = useState({
/** @type {SubtypeData[]} */
subtypes: [],
@@ -60,6 +61,9 @@ export class FollowerSubtypeDialog extends Component {
subtype_ids: selectedSubtypes.map((subtype) => subtype.id),
}
);
if (!selectedSubtypes.some((subtype) => subtype.id === this.store.mt_comment_id)) {
this.env.services["mail.thread"].removeRecipient(this.props.follower);
}
this.env.services.notification.add(
_t("The subscription preferences were successfully applied."),
{ type: "success" }
@@ -18,7 +18,7 @@ export class RecipientList extends Component {
this.threadService = useState(useService("mail.thread"));
this.loadMoreState = useVisible("load-more", () => {
if (this.loadMoreState.isVisible) {
this.threadService.loadMoreFollowers(this.props.thread);
this.threadService.loadMoreRecipients(this.props.thread);
}
});
}
@@ -4,11 +4,10 @@
<t t-name="mail.RecipientList">
<div class="o-mail-RecipientList p-2 overflow-auto">
<ul class="list-unstyled mb-0">
<li t-if="props.thread.selfFollower" t-out="props.thread.selfFollower.partner.email"/>
<li t-foreach="props.thread.followers" t-as="follower" t-key="follower.id">
<t t-out="follower.partner.email"/>
<li t-foreach="props.thread.recipients" t-as="recipient" t-key="recipient.id">
<t t-out="recipient.partner.email"/>
</li>
<span t-if="!props.thread.followersFullyLoaded" class="btn btn-link w-100" t-on-click="() => threadService.loadMoreFollowers(props.thread)" t-ref="load-more">Load more</span>
<span t-if="!props.thread.recipientsFullyLoaded" class="btn btn-link w-100" t-on-click="() => threadService.loadMoreRecipients(props.thread)" t-ref="load-more">Load more</span>
</ul>
</div>
</t>
@@ -5,6 +5,17 @@ import { Thread } from "@mail/core/common/thread_model";
import { patch } from "@web/core/utils/patch";
patch(Thread.prototype, {
/** @type {integer|undefined} */
recipientsCount: undefined,
/** @type {Number} */
mt_comment_id: undefined,
recipients: undefined,
setup() {
this.recipients = new Set();
},
get recipientsFullyLoaded() {
return this.recipientsCount === this.recipients.size;
},
/**
* @returns {import("@mail/core/web/activity_model").Activity[]}
*/
@@ -94,6 +94,15 @@ patch(ThreadService.prototype, {
thread.followers.add(follower);
}
}
thread.recipientsCount = result.recipientsCount;
for (const recipientData of result.recipients) {
thread.recipients.add(
this.store.Follower.insert({
followedThread: thread,
...recipientData,
})
);
}
}
if ("suggestedRecipients" in result) {
this.insertSuggestedRecipients(thread, result.suggestedRecipients);
@@ -180,6 +189,22 @@ patch(ThreadService.prototype, {
}
}
},
async loadMoreRecipients(thread) {
const recipients = await this.orm.call(
thread.model,
"message_get_followers",
[[thread.id], Array.from(thread.recipients).at(-1).id],
{ filter_recipients: true }
);
for (const data of recipients) {
thread.recipients.add(
this.store.Follower.insert({
followedThread: thread,
...data,
})
);
}
},
open(thread, replaceNewMessageChatWindow) {
if (!this.store.discuss.isActive && !this.ui.isSmall) {
this._openChatWindow(thread, replaceNewMessageChatWindow);
@@ -200,6 +225,12 @@ patch(ThreadService.prototype, {
}
super.open(thread, replaceNewMessageChatWindow);
},
/**
* @param {import("@mail/core/common/follower_model").Follower} follower
*/
removeRecipient(recipient) {
recipient.followedThread.recipients.delete(recipient);
},
/**
* @param {import("@mail/core/common/follower_model").Follower} follower
*/
@@ -214,6 +245,7 @@ patch(ThreadService.prototype, {
} else {
thread.followers.delete(follower);
}
this.removeRecipient(follower);
follower.delete();
},
unpin(thread) {
@@ -753,22 +753,6 @@ QUnit.test("remove an uploading attachment", async () => {
await contains(".o-mail-Composer .o-mail-AttachmentCard", { count: 0 });
});
QUnit.test("Show a thread name in the recipient status text.", async () => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "test name", email: "test@odoo.com" });
pyEnv["mail.followers"].create({
is_active: true,
partner_id: partnerId,
res_id: partnerId,
res_model: "res.partner",
});
const { openFormView } = await start();
openFormView("res.partner", partnerId);
await click("button", { text: "Send message" });
await contains(".o-mail-Chatter div", { text: "To: test" });
await contains('span[title="test@odoo.com"]');
});
QUnit.test("Show recipient list when there is more than 5 followers.", async () => {
const pyEnv = await startServer();
const partnerIds = pyEnv["res.partner"].create([
@@ -560,6 +560,14 @@ patch(MockServer.prototype, {
? this._mockMailFollowers_FormatForChatter(selfFollower.id)[0]
: false;
res["followers"] = this._mockMailThreadMessageGetFollowers(thread_model, [thread_id]);
res["recipientsCount"] = (thread.message_follower_ids || []).length - 1;
res["recipients"] = this._mockMailThreadMessageGetFollowers(
thread_model,
[thread_id],
undefined,
100,
{ filter_recipients: true }
);
}
if (request_list.includes("suggestedRecipients")) {
res["suggestedRecipients"] = this._mockMailThread_MessageGetSuggestedRecipients(
@@ -123,7 +123,7 @@ patch(MockServer.prototype, {
* @param {integer} [limit=100]
* @returns {Object[]}
*/
_mockMailThreadMessageGetFollowers(model, ids, after, limit = 100) {
_mockMailThreadMessageGetFollowers(model, ids, after, limit = 100, kwargs = {}) {
const domain = [
["res_id", "=", ids[0]],
["res_model", "=", model],
@@ -131,6 +131,9 @@ patch(MockServer.prototype, {
if (after) {
domain.push(["id", ">", after]);
}
if (kwargs.filter_recipients) {
domain.push(["partner_id", "!=", this.pyEnv.currentPartnerId]);
}
const followers = this.getRecords("mail.followers", domain).sort(
(f1, f2) => (f1.id < f2.id ? -1 : 1) // sorted from lowest ID to highest ID (i.e. from oldest to youngest)
);
@@ -253,11 +253,11 @@ QUnit.test("Load 100 recipients at once", async () => {
);
const { openFormView } = await start();
await openFormView("res.partner", partnerIds[0]);
await contains("button[title='Show Followers']", { text: "210" });
await click("button", { text: "Send message" });
await contains(".o-mail-Chatter div", {
text: "To: me, partner1, partner2, partner3, partner4, …",
});
await contains("button[title='Show Followers']:contains(210)");
await click("button:contains(Send message)");
await contains(
".o-mail-Chatter:contains('partner1, partner2, partner3, partner4, partner5, …')"
);
await contains("button[title='Show all recipients']");
await click("button[title='Show all recipients']");
await contains(".o-mail-RecipientList li", { count: 100 });
@@ -266,7 +266,7 @@ QUnit.test("Load 100 recipients at once", async () => {
await contains(".o-mail-RecipientList li", { count: 200 });
await new Promise(setTimeout); // give enough time for the useVisible hook to register load more as hidden
await scroll(".o-mail-RecipientList", "bottom");
await contains(".o-mail-RecipientList li", { count: 210 });
await contains(".o-mail-RecipientList li", { count: 209 });
await contains(".o-mail-RecipientList span", { count: 0, text: "Load more" });
});
@@ -1008,6 +1008,7 @@ class TestDiscussFullPerformance(HttpCase):
],
'internalUserGroupId': self.env.ref('base.group_user').id,
'menu_id': self.env['ir.model.data']._xmlid_to_res_id('mail.menu_root_discuss'),
'mt_comment_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment'),
'odoobot': {
'active': False,
'email': 'odoobot@example.com',