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']");