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