From 6aa3a1e243f6e0f798c35cce97c7955e415c4b83 Mon Sep 17 00:00:00 2001 From: "Didier (did)" Date: Thu, 24 Aug 2023 14:04:37 +0200 Subject: [PATCH] [IMP] mail: message action registry + reorder actions in inbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, message actions were all defined in template of `Message` component. It's good when there are few actions and the ordering of actions is always the same, but there are now many actions and we want to order these actions depending on context. This commit fixes the issue by introducing a special registry to add message actions. This registry is quite similar to thread actions: each item define conditions to be shown, behaviour on click, and their sequence which can depend on the context where the message is being viewed. In addition to this message action registry, this commit also reorder message actions in inbox as follow: 1. Quick actions: - Add a Reaction - Mark as To do - Mark as Read 2. More menu: - View reactions - Reply - Unfollow - Edit - Delete Task-3458086 closes odoo/odoo#131426 Signed-off-by: Alexandre Kühn (aku) --- addons/mail/static/src/core/common/message.js | 22 +-- .../mail/static/src/core/common/message.xml | 25 ++-- .../static/src/core/common/message_actions.js | 131 ++++++++++++++++++ .../core/common/message_reaction_button.js | 32 +++++ .../core/common/message_reaction_button.xml | 8 ++ .../message_pin/common/message_actions.js | 12 ++ .../message_pin/common/message_patch.xml | 15 -- .../static/tests/discuss_app/discuss_tests.js | 5 +- .../static/tests/discuss_app/inbox_tests.js | 7 +- 9 files changed, 210 insertions(+), 47 deletions(-) create mode 100644 addons/mail/static/src/core/common/message_actions.js create mode 100644 addons/mail/static/src/core/common/message_reaction_button.js create mode 100644 addons/mail/static/src/core/common/message_reaction_button.xml create mode 100644 addons/mail/static/src/discuss/message_pin/common/message_actions.js delete mode 100644 addons/mail/static/src/discuss/message_pin/common/message_patch.xml diff --git a/addons/mail/static/src/core/common/message.js b/addons/mail/static/src/core/common/message.js index 7a49fa4ecee..f0df2545fb4 100644 --- a/addons/mail/static/src/core/common/message.js +++ b/addons/mail/static/src/core/common/message.js @@ -2,7 +2,6 @@ import { AttachmentList } from "@mail/core/common/attachment_list"; import { Composer } from "@mail/core/common/composer"; -import { useEmojiPicker } from "@web/core/emoji_picker/emoji_picker"; import { ImStatus } from "@mail/core/common/im_status"; import { LinkPreviewList } from "@mail/core/common/link_preview_list"; import { MessageConfirmDialog } from "@mail/core/common/message_confirm_dialog"; @@ -34,6 +33,7 @@ import { _t } from "@web/core/l10n/translation"; import { usePopover } from "@web/core/popover/popover_hook"; import { useService } from "@web/core/utils/hooks"; import { url } from "@web/core/utils/urls"; +import { useMessageActions } from "./message_actions"; /** * @typedef {Object} Props @@ -91,6 +91,7 @@ export class Message extends Component { this.root = useRef("root"); this.hasTouch = hasTouch; this.messageBody = useRef("body"); + this.messageActions = useMessageActions(); this.store = useState(useService("mail.store")); this.rpc = useService("rpc"); this.threadService = useState(useService("mail.thread")); @@ -116,21 +117,6 @@ export class Message extends Component { this.root.el.scrollIntoView({ behavior: "smooth", block: "center" }); } }); - this.emojiPickerRef = useRef("emoji-picker"); - if (this.props.hasActions && this.canAddReaction) { - this.emojiPicker = useEmojiPicker(this.emojiPickerRef, { - onSelect: (emoji) => { - const reaction = this.message.reactions.find( - ({ content, personas }) => - content === emoji && - personas.find((persona) => persona.eq(this.store.self)) - ); - if (!reaction) { - this.messageService.react(this.message, emoji); - } - }, - }); - } onMounted(() => { if (this.messageBody.el) { $(this.messageBody.el).find(".o-mail-read-more-less").remove(); @@ -189,6 +175,10 @@ export class Message extends Component { return this.props.message; } + get quickActionCount() { + return this.env.inChatter ? 2 : 3; + } + get showSubtypeDescription() { return ( this.message.subtypeDescription && diff --git a/addons/mail/static/src/core/common/message.xml b/addons/mail/static/src/core/common/message.xml index a460d756111..b19e4b567ae 100644 --- a/addons/mail/static/src/core/common/message.xml +++ b/addons/mail/static/src/core/common/message.xml @@ -135,22 +135,23 @@ 'flex-row-reverse': env.inChatWindow and isAlignedRight, }" > - - - - - - + + + + + - View Reactions - Add a Reaction - Unfollow - Mark as Unread - Edit - Delete + + + + + + diff --git a/addons/mail/static/src/core/common/message_actions.js b/addons/mail/static/src/core/common/message_actions.js new file mode 100644 index 00000000000..1331a662971 --- /dev/null +++ b/addons/mail/static/src/core/common/message_actions.js @@ -0,0 +1,131 @@ +/* @odoo-module */ + +import { useComponent, useState } from "@odoo/owl"; + +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { MessageReactionButton } from "./message_reaction_button"; + +export const messageActionsRegistry = registry.category("mail.message/actions"); + +messageActionsRegistry + .add("reaction", { + callComponent: MessageReactionButton, + props: (component) => ({ message: component.props.message }), + condition: (component) => component.canAddReaction, + sequence: 10, + }) + .add("reply-to", { + condition: (component) => component.canReplyTo, + icon: "fa-reply", + title: _t("Reply"), + onClick: (component) => component.onClickReplyTo(), + sequence: (component) => (component.isInInbox ? 55 : 20), + }) + .add("toggle-star", { + condition: (component) => component.canToggleStar, + icon: (component) => + component.props.message.isStarred ? "fa-star o-mail-Message-starred" : "fa-star-o", + title: _t("Mark as Todo"), + onClick: (component) => component.messageService.toggleStar(component.props.message), + sequence: 30, + }) + .add("mark-as-read", { + condition: (component) => component.isInInbox, + icon: "fa-check", + title: _t("Mark as Read"), + onClick: (component) => component.messageService.setDone(component.props.message), + sequence: 40, + }) + .add("reactions", { + condition: (component) => component.message.reactions.length, + icon: "fa-smile-o", + title: _t("View Reactions"), + onClick: (component) => component.openReactionMenu(), + sequence: 50, + dropdown: true, + }) + .add("unfollow", { + condition: (component) => component.showUnfollow, + icon: "fa-user-times", + title: _t("Unfollow"), + onClick: (component) => component.messageService.unfollow(component.props.message), + sequence: 60, + }) + .add("mark-as-unread", { + condition: (component) => + component.props.thread.model === "discuss.channel" && component.store.user, + icon: "fa-eye-slash", + title: _t("Mark as Unread"), + onClick: (component) => component.onClickMarkAsUnread(), + sequence: 70, + }) + .add("edit", { + condition: (component) => component.editable, + icon: "fa-pencil", + title: _t("Edit"), + onClick: (component) => component.onClickEdit(), + sequence: 80, + }) + .add("delete", { + condition: (component) => component.deletable, + icon: "fa-trash", + title: _t("Delete"), + onClick: (component) => component.onClickDelete(), + sequence: 90, + }); + +function transformAction(component, id, action) { + return { + component: action.component, + id, + /** Condition to display this action. */ + get condition() { + return action.condition(component); + }, + /** Icon for the button this action. */ + get icon() { + return typeof action.icon === "function" ? action.icon(component) : action.icon; + }, + /** title of this action, displayed to the user. */ + get title() { + return typeof action.title === "function" ? action.title(component) : action.title; + }, + callComponent: action.callComponent, + get props() { + return action.props(component); + }, + /** + * Action to execute when this action is click. + * + * @param {object} [param0] + * @param {boolean} [param0.keepPrevious] Whether the previous action + * should be kept so that closing the current action goes back + * to the previous one. + * */ + onClick() { + action.onClick?.(component); + }, + /** Determines the order of this action (smaller first). */ + get sequence() { + return typeof action.sequence === "function" + ? action.sequence(component) + : action.sequence; + }, + }; +} + +export function useMessageActions() { + const component = useComponent(); + const transformedActions = messageActionsRegistry + .getEntries() + .map(([id, action]) => transformAction(component, id, action)); + const state = useState({ + get actions() { + return transformedActions + .filter((action) => action.condition) + .sort((a1, a2) => a1.sequence - a2.sequence); + }, + }); + return state; +} diff --git a/addons/mail/static/src/core/common/message_reaction_button.js b/addons/mail/static/src/core/common/message_reaction_button.js new file mode 100644 index 00000000000..c19d14e6041 --- /dev/null +++ b/addons/mail/static/src/core/common/message_reaction_button.js @@ -0,0 +1,32 @@ +/* @odoo-module */ + +import { Component, useRef, useState } from "@odoo/owl"; +import { useEmojiPicker } from "@web/core/emoji_picker/emoji_picker"; +import { useService } from "@web/core/utils/hooks"; + +/** + * @typedef {Object} Props + * @property {import("@mail/core/common/message_model").Message} message + * @extends {Component} + */ +export class MessageReactionButton extends Component { + static template = "mail.MessageReactionButton"; + static props = ["message"]; + + setup() { + this.messageService = useState(useService("mail.message")); + this.store = useState(useService("mail.store")); + this.emojiPickerRef = useRef("emoji-picker"); + this.emojiPicker = useEmojiPicker(this.emojiPickerRef, { + onSelect: (emoji) => { + const reaction = this.props.message.reactions.find( + ({ content, personas }) => + content === emoji && personas.find((persona) => persona.eq(this.store.self)) + ); + if (!reaction) { + this.messageService.react(this.props.message, emoji); + } + }, + }); + } +} diff --git a/addons/mail/static/src/core/common/message_reaction_button.xml b/addons/mail/static/src/core/common/message_reaction_button.xml new file mode 100644 index 00000000000..8f0049b7376 --- /dev/null +++ b/addons/mail/static/src/core/common/message_reaction_button.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/mail/static/src/discuss/message_pin/common/message_actions.js b/addons/mail/static/src/discuss/message_pin/common/message_actions.js new file mode 100644 index 00000000000..89c9f6fe16e --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/common/message_actions.js @@ -0,0 +1,12 @@ +/* @odoo-module */ + +import { messageActionsRegistry } from "@mail/core/common/message_actions"; + +messageActionsRegistry.add("pin", { + condition: (component) => + component.store.user && component.props.thread.model === "discuss.channel", + icon: "fa-thumb-tack", + title: (component) => component.getPinOptionText(), + onClick: (component) => component.onClickPin(), + sequence: 65, +}); diff --git a/addons/mail/static/src/discuss/message_pin/common/message_patch.xml b/addons/mail/static/src/discuss/message_pin/common/message_patch.xml deleted file mode 100644 index 7faa59a5251..00000000000 --- a/addons/mail/static/src/discuss/message_pin/common/message_patch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - diff --git a/addons/mail/static/tests/discuss_app/discuss_tests.js b/addons/mail/static/tests/discuss_app/discuss_tests.js index 52ec5c49918..e26d4f12e0a 100644 --- a/addons/mail/static/tests/discuss_app/discuss_tests.js +++ b/addons/mail/static/tests/discuss_app/discuss_tests.js @@ -328,6 +328,7 @@ QUnit.test("reply to message from inbox (message linked to document) [REQUIRE FO openDiscuss(); await contains(".o-mail-Message"); await contains(".o-mail-Message-header small", { text: "on Refactoring" }); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Message.o-selected"); await contains(".o-mail-Composer"); @@ -613,17 +614,15 @@ QUnit.test("rendering of inbox message", async () => { await contains(".o-mail-Message"); await contains(".o-mail-Message-header small", { text: "on Refactoring" }); await contains(".o-mail-Message-actions i", { count: 4 }); - await contains("[title='Reply']"); + await contains("[title='Add a Reaction']"); await contains("[title='Mark as Todo']"); await contains("[title='Mark as Read']"); - await contains("[title='Expand']"); await click("[title='Expand']"); await contains(".o-mail-Message-actions i", { count: 5 }); await contains("[title='Reply']"); await contains("[title='Mark as Todo']"); await contains("[title='Mark as Read']"); await contains("[title='Expand']"); - await contains("[title='Add a Reaction']"); }); QUnit.test("Unfollow message", async function () { diff --git a/addons/mail/static/tests/discuss_app/inbox_tests.js b/addons/mail/static/tests/discuss_app/inbox_tests.js index cd0f7c6679f..630afbd184d 100644 --- a/addons/mail/static/tests/discuss_app/inbox_tests.js +++ b/addons/mail/static/tests/discuss_app/inbox_tests.js @@ -26,8 +26,10 @@ QUnit.test("reply: discard on reply button toggle", async (assert) => { openDiscuss(); await contains(".o-mail-Message"); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Composer"); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Composer", { count: 0 }); }); @@ -54,7 +56,7 @@ QUnit.test("reply: discard on pressing escape", async () => { const { openDiscuss } = await start(); openDiscuss(); await contains(".o-mail-Message"); - + await click(".o-mail-Message [title='Expand']"); await click(".o-mail-Message [title='Reply']"); await contains(".o-mail-Composer"); @@ -107,6 +109,7 @@ QUnit.test( }); openDiscuss(); await contains(".o-mail-Message"); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Composer [placeholder='Log an internal note…']"); await insertText(".o-mail-Composer-input", "Test"); @@ -146,6 +149,7 @@ QUnit.test( }); openDiscuss(); await contains(".o-mail-Message"); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Composer [placeholder='Send a message to followers…']"); await insertText(".o-mail-Composer-input", "Test"); @@ -552,6 +556,7 @@ QUnit.test("reply: stop replying button click", async () => { openDiscuss(); await contains(".o-mail-Message"); + await click("[title='Expand']"); await click("[title='Reply']"); await contains(".o-mail-Composer"); await contains("i[title='Stop replying']");