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" });