diff --git a/addons/mail/static/src/core/common/persona_model.js b/addons/mail/static/src/core/common/persona_model.js
index a4a4eb1b426..dfe58920155 100644
--- a/addons/mail/static/src/core/common/persona_model.js
+++ b/addons/mail/static/src/core/common/persona_model.js
@@ -36,4 +36,8 @@ export class Persona {
get nameOrDisplayName() {
return this.name || this.displayName;
}
+
+ get emailWithoutDomain() {
+ return this.email.substring(0, this.email.lastIndexOf("@"));
+ }
}
diff --git a/addons/mail/static/src/core/web/chatter.js b/addons/mail/static/src/core/web/chatter.js
index 092d96ce35f..baaeac65846 100644
--- a/addons/mail/static/src/core/web/chatter.js
+++ b/addons/mail/static/src/core/web/chatter.js
@@ -27,10 +27,12 @@ import {
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, sprintf } from "@web/core/utils/strings";
+import { escape } from "@web/core/utils/strings";
import { useThrottleForAnimation } from "@web/core/utils/timing";
import { FileUploader } from "@web/views/fields/file_handler";
+import { RecipientList } from "./recipient_list";
/**
* @typedef {Object} Props
@@ -105,6 +107,7 @@ export class Chatter extends Component {
this.scrollPosition = useScrollPosition("root", undefined, "top");
this.rootRef = useRef("root");
this.onScrollDebounced = useThrottleForAnimation(this.onScroll);
+ this.recipientsPopover = usePopover(RecipientList);
useChildSubEnv({ inChatter: true });
useDropzone(
this.rootRef,
@@ -197,19 +200,23 @@ export class Chatter extends Component {
/**
* @returns {string}
*/
- get toFollowersText() {
- const threadName = this.state.thread.displayName || this.state.thread.name;
- const toFollowersText = threadName
- ? sprintf(
- _t(
- 'To: Followers of "%(thread name)s".'
- ),
- { "thread name": escape(threadName) }
- )
- : _t(
- 'To: Followers of this document.'
- );
- return markup(toFollowersText);
+ get toRecipientsText() {
+ const followers = this.state.thread.followers.slice(0, 5).map(({ partner }) => {
+ if (partner === this.store.self) {
+ return `me`;
+ }
+ return `${escape(
+ partner.emailWithoutDomain
+ )}`;
+ });
+ const formatter = new Intl.ListFormat(
+ this.store.env.services["user"].lang?.replace("_", "-"),
+ { type: "unit" }
+ );
+ if (this.state.thread.followers.length > 5) {
+ followers.push("…");
+ }
+ return markup(formatter.format(followers));
}
/**
@@ -409,4 +416,11 @@ export class Chatter extends Component {
onScroll() {
this.state.isTopStickyPinned = this.rootRef.el.scrollTop !== 0;
}
+
+ onClickRecipientList(ev) {
+ if (this.recipientsPopover.isOpen) {
+ return this.recipientsPopover.close();
+ }
+ this.recipientsPopover.open(ev.target, { thread: this.state.thread });
+ }
}
diff --git a/addons/mail/static/src/core/web/chatter.scss b/addons/mail/static/src/core/web/chatter.scss
index 5f4ea2fa7f4..950e6508e56 100644
--- a/addons/mail/static/src/core/web/chatter.scss
+++ b/addons/mail/static/src/core/web/chatter.scss
@@ -19,6 +19,10 @@
}
}
+.o-mail-Chatter-recipientListButton:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
.o-mail-Followers-button:focus {
background-color: $gray-200;
}
diff --git a/addons/mail/static/src/core/web/chatter.xml b/addons/mail/static/src/core/web/chatter.xml
index 75c2c5715d3..c9403fec8d4 100644
--- a/addons/mail/static/src/core/web/chatter.xml
+++ b/addons/mail/static/src/core/web/chatter.xml
@@ -92,8 +92,13 @@
-
-
+
+
+ To:
+
+
Send a message to followers...
Log an internal note...
diff --git a/addons/mail/static/src/core/web/recipient_list.js b/addons/mail/static/src/core/web/recipient_list.js
new file mode 100644
index 00000000000..e3197088d0b
--- /dev/null
+++ b/addons/mail/static/src/core/web/recipient_list.js
@@ -0,0 +1,14 @@
+/* @odoo-module */
+
+import { Component } from "@odoo/owl";
+
+/**
+ * @typedef {Object} Props
+ * @property {import('@mail/core/common/thread_model').Thread} thread
+ * @property {function} [close]
+ * @extends {Component}
+ */
+export class RecipientList extends Component {
+ static template = "mail.RecipientList";
+ static props = ["thread", "close?"];
+}
diff --git a/addons/mail/static/src/core/web/recipient_list.scss b/addons/mail/static/src/core/web/recipient_list.scss
new file mode 100644
index 00000000000..7ef606a91e7
--- /dev/null
+++ b/addons/mail/static/src/core/web/recipient_list.scss
@@ -0,0 +1,3 @@
+.o-mail-RecipientList {
+ max-height: 410px;
+}
diff --git a/addons/mail/static/src/core/web/recipient_list.xml b/addons/mail/static/src/core/web/recipient_list.xml
new file mode 100644
index 00000000000..6a5397e59e7
--- /dev/null
+++ b/addons/mail/static/src/core/web/recipient_list.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/tests/composer/composer_tests.js b/addons/mail/static/tests/composer/composer_tests.js
index 39e6464b16a..f1c48d0513b 100644
--- a/addons/mail/static/tests/composer/composer_tests.js
+++ b/addons/mail/static/tests/composer/composer_tests.js
@@ -827,25 +827,52 @@ QUnit.test("remove an uploading attachment", async (assert) => {
assert.containsNone($, ".o-mail-Composer .o-mail-AttachmentCard");
});
-QUnit.test(
- "Show a default status in the recipient status text when the thread doesn't have a name.",
- async (assert) => {
- const pyEnv = await startServer();
- const partnerId = pyEnv["res.partner"].create({});
- const { openFormView } = await start();
- await openFormView("res.partner", partnerId);
- await click("button:contains(Send message)");
- assert.containsOnce($, ".o-mail-Chatter:contains(To: Followers of this document)");
- }
-);
-
QUnit.test("Show a thread name in the recipient status text.", async (assert) => {
const pyEnv = await startServer();
- const partnerId = pyEnv["res.partner"].create({ name: "test name" });
+ 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();
await openFormView("res.partner", partnerId);
await click("button:contains(Send message)");
- assert.containsOnce($, '.o-mail-Chatter:contains(To: Followers of "test name")');
+ assert.containsOnce($, ".o-mail-Chatter:contains(To: test)");
+ assert.containsOnce($, 'span[title="test@odoo.com"]');
+});
+
+QUnit.test("Show follower list when there is more than 5 followers.", async (assert) => {
+ const pyEnv = await startServer();
+ const partnerIds = pyEnv["res.partner"].create([
+ { name: "test name 1", email: "test1@odoo.com" },
+ { name: "test name 2", email: "test2@odoo.com" },
+ { name: "test name 3", email: "test3@odoo.com" },
+ { name: "test name 4", email: "test4@odoo.com" },
+ { name: "test name 5", email: "test5@odoo.com" },
+ { name: "test name 6", email: "test6@odoo.com" },
+ ]);
+ for (const partner of partnerIds) {
+ pyEnv["mail.followers"].create({
+ is_active: true,
+ partner_id: partner,
+ res_id: partnerIds[0],
+ res_model: "res.partner",
+ });
+ }
+ const { openFormView } = await start();
+ await openFormView("res.partner", partnerIds[0]);
+ await click("button:contains(Send message)");
+ assert.containsOnce($, "button[title='Show all recipients']");
+ await click("button[title='Show all recipients']");
+ assert.containsOnce($, "li:contains('test1@odoo.com')");
+ assert.containsOnce($, "li:contains('test2@odoo.com')");
+ assert.containsOnce($, "li:contains('test3@odoo.com')");
+ assert.containsOnce($, "li:contains('test4@odoo.com')");
+ assert.containsOnce($, "li:contains('test5@odoo.com')");
+ assert.containsOnce($, "li:contains('test6@odoo.com')");
+ assert.containsOnce($, ".o-mail-Chatter:contains('test1, test2, test3, test4, test5, …')");
});
QUnit.test(
diff --git a/addons/mail/static/tests/web/chatter_tests.js b/addons/mail/static/tests/web/chatter_tests.js
index 273952ba734..8d34cfc5dd1 100644
--- a/addons/mail/static/tests/web/chatter_tests.js
+++ b/addons/mail/static/tests/web/chatter_tests.js
@@ -40,14 +40,6 @@ QUnit.test("simple chatter on a record", async (assert) => {
]);
});
-QUnit.test("displayname is used when sending a message", async (assert) => {
- const { openFormView, pyEnv } = await start();
- const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });
- await openFormView("res.partner", partnerId);
- await click("button:contains(Send message)");
- assert.containsOnce($, '.o-mail-Chatter:contains(To: Followers of "John Doe")');
-});
-
QUnit.test("can post a message on a record thread", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "John Doe" });