From 80de5298b2164f85708e4b1b5d08e13591e7787c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Wed, 17 May 2023 09:37:35 +0000 Subject: [PATCH] [REF] mail: move message pin to discuss folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of task-3265211 closes odoo/odoo#121418 Signed-off-by: Alexandre Kühn (aku) --- addons/mail/__manifest__.py | 4 +- addons/mail/models/mail_thread.py | 1 + addons/mail/static/src/core/message_model.js | 9 - .../mail/static/src/core/message_service.js | 14 +- .../mail/static/src/core/messaging_service.js | 17 -- addons/mail/static/src/core/thread_model.js | 4 - addons/mail/static/src/core/thread_service.js | 25 --- addons/mail/static/src/core_ui/message.js | 44 ++-- addons/mail/static/src/core_ui/message.scss | 2 +- addons/mail/static/src/core_ui/message.xml | 14 +- addons/mail/static/src/core_ui/thread.js | 3 - addons/mail/static/src/core_ui/thread.scss | 4 - addons/mail/static/src/core_ui/thread.xml | 3 +- .../mail/static/src/discuss/discuss_patch.xml | 2 +- .../src/discuss/message_pin/discuss_patch.js | 32 +++ .../src/discuss/message_pin/discuss_patch.xml | 13 ++ .../src/discuss/message_pin/message_patch.js | 55 +++++ .../src/discuss/message_pin/message_patch.xml | 15 ++ .../message_pin/message_pin_service.js | 198 ++++++++++++++++++ .../message_pin}/pinned_messages_panel.js | 15 +- .../message_pin/pinned_messages_panel.scss | 19 ++ .../message_pin/pinned_messages_panel.xml | 26 +++ .../src/discuss/message_pin/thread_patch.js | 23 ++ .../src/discuss/message_pin/thread_patch.scss | 3 + .../src/discuss/message_pin/thread_patch.xml | 8 + .../message_pin/web/chat_window_patch.js | 32 +++ .../message_pin/web/chat_window_patch.xml | 13 ++ .../src/discuss/web/chat_window_patch.xml | 8 +- addons/mail/static/src/discuss_app/discuss.js | 33 +-- .../mail/static/src/discuss_app/discuss.xml | 6 +- .../discuss_app/pinned_messages_panel.scss | 19 -- .../src/discuss_app/pinned_messages_panel.xml | 27 --- .../static/src/web/chat_window/chat_window.js | 23 +- .../src/web/chat_window/chat_window.xml | 8 +- .../message_pin}/pinned_messages_tests.js | 18 +- .../message_pin/setup_manager_patch.js | 14 ++ 36 files changed, 519 insertions(+), 235 deletions(-) create mode 100644 addons/mail/static/src/discuss/message_pin/discuss_patch.js create mode 100644 addons/mail/static/src/discuss/message_pin/discuss_patch.xml create mode 100644 addons/mail/static/src/discuss/message_pin/message_patch.js create mode 100644 addons/mail/static/src/discuss/message_pin/message_patch.xml create mode 100644 addons/mail/static/src/discuss/message_pin/message_pin_service.js rename addons/mail/static/src/{discuss_app => discuss/message_pin}/pinned_messages_panel.js (84%) create mode 100644 addons/mail/static/src/discuss/message_pin/pinned_messages_panel.scss create mode 100644 addons/mail/static/src/discuss/message_pin/pinned_messages_panel.xml create mode 100644 addons/mail/static/src/discuss/message_pin/thread_patch.js create mode 100644 addons/mail/static/src/discuss/message_pin/thread_patch.scss create mode 100644 addons/mail/static/src/discuss/message_pin/thread_patch.xml create mode 100644 addons/mail/static/src/discuss/message_pin/web/chat_window_patch.js create mode 100644 addons/mail/static/src/discuss/message_pin/web/chat_window_patch.xml delete mode 100644 addons/mail/static/src/discuss_app/pinned_messages_panel.scss delete mode 100644 addons/mail/static/src/discuss_app/pinned_messages_panel.xml rename addons/mail/static/tests/{discuss_app => discuss/message_pin}/pinned_messages_tests.js (87%) create mode 100644 addons/mail/static/tests/helpers/discuss/message_pin/setup_manager_patch.js diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index a8d293f5475..f5d58f6243d 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -184,13 +184,13 @@ For more specific needs, you may also assign custom-defined actions ('remove', 'mail/static/src/xml/**/*'), ('remove', 'mail/static/src/worklets/**/*'), ('remove', 'mail/static/src/views/**/*'), - ('remove', 'mail/static/src/web/**/*'), + ('remove', 'mail/static/src/**/web/**/*'), ('remove', 'mail/static/src/emoji_picker/emoji_data.js'), ('remove', 'mail/static/src/**/*.dark.scss'), # discuss (loaded last to fix dependencies) ('remove', 'mail/static/src/discuss/**/*'), 'mail/static/src/discuss/**/*', - ('remove', 'mail/static/src/discuss/web/**/*'), + ('remove', 'mail/static/src/discuss/**/web/**/*'), ] }, 'license': 'LGPL-3', diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 1f08f6504a7..369b6803b21 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -4088,6 +4088,7 @@ class MailThread(models.AbstractModel): 'id': message.id, 'body': message.body, 'attachment_ids': message.attachment_ids._attachment_format(), + 'pinned_at': message.pinned_at, 'write_date': message.write_date, } }) diff --git a/addons/mail/static/src/core/message_model.js b/addons/mail/static/src/core/message_model.js index ffe2e92cb2d..ed7c66c50df 100644 --- a/addons/mail/static/src/core/message_model.js +++ b/addons/mail/static/src/core/message_model.js @@ -66,8 +66,6 @@ export class Message { /** @type {string|undefined} */ notificationType; /** @type {string} */ - pinned_at; - /** @type {string} */ create_date; /** @type {string} */ write_date; @@ -237,13 +235,6 @@ export class Message { return this.notifications.filter((notification) => notification.isFailure); } - get pinnedAt() { - if (!this.pinned_at) { - return null; - } - return luxon.DateTime.fromISO(new Date(this.pinned_at).toISOString()); - } - get editDatetimeHuge() { return deserializeDateTime(this.editDate).toLocaleString( omit(DateTime.DATETIME_HUGE, "timeZoneName") diff --git a/addons/mail/static/src/core/message_service.js b/addons/mail/static/src/core/message_service.js index 640b6a70f79..d9570eddb9c 100644 --- a/addons/mail/static/src/core/message_service.js +++ b/addons/mail/static/src/core/message_service.js @@ -136,13 +136,6 @@ export class MessageService { await this.orm.silent.call("mail.message", "set_message_done", [[message.id]]); } - setPin(message, pinned) { - return this.orm.call("discuss.channel", "set_message_pin", [message.originThread.id], { - message_id: message.id, - pinned, - }); - } - async unfollow(message) { if (message.isNeedaction) { await this.setDone(message); @@ -232,12 +225,6 @@ export class MessageService { * @param {Object} data */ update(message, data) { - if (message.pinned_at && data.pinned_at === false) { - removeFromArrayWithPredicate( - message.originThread.pinnedMessages, - ({ id }) => id === message.id - ); - } const { attachment_ids: attachments = message.attachments, default_subject: defaultSubject = message.defaultSubject, @@ -333,6 +320,7 @@ export class MessageService { message.notificationType = htmlBody.querySelector(".o_mail_notification")?.dataset.oeType; } + this.env.bus.trigger("mail.message/onUpdate", { message, data }); } _updateReactions(message, reactionGroups) { diff --git a/addons/mail/static/src/core/messaging_service.js b/addons/mail/static/src/core/messaging_service.js index 6339d71b536..2996585db96 100644 --- a/addons/mail/static/src/core/messaging_service.js +++ b/addons/mail/static/src/core/messaging_service.js @@ -310,10 +310,6 @@ export class Messaging { ({ id }) => id === message.id ); } - removeFromArrayWithPredicate( - message.originThread.pinnedMessages, - ({ id }) => id === message.id - ); if (message.id > message.originThread.seen_message_id) { message.originThread.message_unread_counter--; } @@ -587,22 +583,9 @@ export class Messaging { ...messageData, body: messageData.body ? markup(messageData.body) : messageData.body, }); - if ( - message.pinned_at && - !message.originThread?.pinnedMessages.some(({ id }) => id === message.id) - ) { - message.originThread.pinnedMessages.unshift(message); - } if (isStarred && message.isEmpty) { this.messageService.updateStarred(message, false); } - if (message.pinned_at && message.isEmpty) { - message.pinned_at = false; - removeFromArrayWithPredicate( - message.originThread.pinnedMessages, - ({ id }) => id === message.id - ); - } } const { "res.users.settings": settings } = notif.payload; if (settings) { diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index f9c482ae90b..8901f28e83d 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -59,8 +59,6 @@ export class Thread { isLoadingAttachments = false; isLoadedDeferred = new Deferred(); isLoaded = false; - /** @type {"loading"|"loaded"} */ - pinLoadState; /** @type {import("@mail/attachments/attachment_model").Attachment} */ mainAttachment; memberCount = 0; @@ -98,8 +96,6 @@ export class Thread { * @type {import("@mail/core/message_model").Message[]} */ needactionMessages = []; - /** @type {import("@mail/core/message_model").Message[]} */ - pinnedMessages = []; /** @type {string} */ name; /** @type {number|false} */ diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index 4f9c6e8c5cb..52496db17ec 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -160,31 +160,6 @@ export class ThreadService { await this.orm.silent.call("discuss.channel", "channel_fetched", [[thread.id]]); } - async fetchPinnedMessages(thread) { - if ( - thread.model !== "discuss.channel" || - ["loaded", "loading"].includes(thread.pinLoadState) - ) { - return; - } - thread.pinLoadState = "loading"; - try { - const messages = await this.rpc("/discuss/channel/pinned_messages", { - channel_id: thread.id, - }); - const pinnedMessages = messages.map((message) => { - if (message.parentMessage) { - message.parentMessage.body = markup(message.parentMessage.body); - } - message.body = markup(message.body); - return this.messageService.insert(message); - }); - thread.pinnedMessages = pinnedMessages; - } finally { - thread.pinLoadState = "loaded"; - } - } - getFetchRoute(thread) { if (thread.model === "discuss.channel") { return "/discuss/channel/messages"; diff --git a/addons/mail/static/src/core_ui/message.js b/addons/mail/static/src/core_ui/message.js index 9b1ab2a4971..8f571ba10b4 100644 --- a/addons/mail/static/src/core_ui/message.js +++ b/addons/mail/static/src/core_ui/message.js @@ -145,6 +145,26 @@ export class Message extends Component { }); } + get attClass() { + return { + "o-highlighted-from-mention": this.message.isHighlightedFromMention, + "o-highlighted bg-view shadow-lg": this.props.highlighted, + "o-selfAuthored": this.message.isSelfAuthored, + "o-selected": this.props.messageToReplyTo?.isSelected( + this.props.thread, + this.props.message + ), + "o-squashed pt-1": this.props.squashed, + "mt-1": !this.props.squashed && this.props.thread, + "px-3": !this.props.isInChatWindow, + "px-1": this.props.isInChatWindow, + "opacity-50": this.props.messageToReplyTo?.isNotSelected( + this.props.thread, + this.props.message + ), + }; + } + get authorAvatarUrl() { if ( this.message.type === "email" && @@ -260,9 +280,7 @@ export class Message extends Component { get isAlignedRight() { return Boolean( - !this.env.pinnedPanel && - this.env.inChatWindow && - this.user.partnerId === this.props.message.author?.id + this.env.inChatWindow && this.user.partnerId === this.props.message.author?.id ); } @@ -293,7 +311,7 @@ export class Message extends Component { * @returns {boolean} */ get shouldDisplayAuthorName() { - if (!this.env.inChatWindow || this.env.pinnedPanel) { + if (!this.env.inChatWindow) { return true; } if (this.message.isSelfAuthored) { @@ -314,20 +332,6 @@ export class Message extends Component { }); } - onClickPin() { - const prompt = this.message.pinned_at - ? _t("Are you sure you want to remove this pinned message?") - : _t( - "The following message will be pinned to the channel. Are you sure you want to continue?" - ); - this.env.services.dialog.add(MessageConfirmDialog, { - message: this.message, - messageComponent: Message, - prompt, - onConfirm: () => this.messageService.setPin(this.message, !this.message.pinned_at), - }); - } - onClickReplyTo(ev) { this.props.messageToReplyTo.toggle(this.props.thread, this.props.message); } @@ -359,10 +363,6 @@ export class Message extends Component { return this.hasOpenChatFeature ? _t("Open chat") : ""; } - get pinOptionText() { - return this.message.pinned_at ? _t("Unpin") : _t("Pin"); - } - openChatAvatar(ev) { markEventHandled(ev, "Message.ClickAuthor"); if (!this.hasOpenChatFeature) { diff --git a/addons/mail/static/src/core_ui/message.scss b/addons/mail/static/src/core_ui/message.scss index 27e92b3af22..f3f228cc0ff 100644 --- a/addons/mail/static/src/core_ui/message.scss +++ b/addons/mail/static/src/core_ui/message.scss @@ -32,7 +32,7 @@ } } -.o-mail-ChatWindow .o-mail-Message:not(.o-pinnedPanel).o-selfAuthored { +.o-mail-ChatWindow .o-mail-Message:not(.o-cancelSelfAuthored).o-selfAuthored { flex-direction: row-reverse; .o-mail-Message-core, .o-mail-Message-content { diff --git a/addons/mail/static/src/core_ui/message.xml b/addons/mail/static/src/core_ui/message.xml index a640c1c56c6..0e054fc1914 100644 --- a/addons/mail/static/src/core_ui/message.xml +++ b/addons/mail/static/src/core_ui/message.xml @@ -4,18 +4,7 @@
View Reactions Mark as Read Unfollow - Mark as Unread Edit Delete diff --git a/addons/mail/static/src/core_ui/thread.js b/addons/mail/static/src/core_ui/thread.js index f3c760e569b..9813399197b 100644 --- a/addons/mail/static/src/core_ui/thread.js +++ b/addons/mail/static/src/core_ui/thread.js @@ -52,7 +52,6 @@ export class Thread extends Component { this.state = useState({ isReplyingTo: false, showJumpPresent: false }); /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useState(useService("mail.thread")); - this.ui = useState(useService("ui")); if (!this.env.inChatter || !this.props.hasScrollAdjust) { useAutoScroll("messages", () => { if (this.env.messageHighlight?.highlightedMessageId) { @@ -174,8 +173,6 @@ export class Thread extends Component { const { oeType, oeId } = ev.target.dataset; if (oeType === "highlight") { await this.env.messageHighlight?.highlightMessage(Number(oeId), this.props.thread); - } else if (oeType === "pin-menu") { - this.env.pinMenu?.open(); } } diff --git a/addons/mail/static/src/core_ui/thread.scss b/addons/mail/static/src/core_ui/thread.scss index c170d2c2875..27c4fe66d75 100644 --- a/addons/mail/static/src/core_ui/thread.scss +++ b/addons/mail/static/src/core_ui/thread.scss @@ -21,10 +21,6 @@ display: inline; } -.o-mail-Thread-noPinnedPanel [data-oe-type="pin-menu"] { - display: none; -} - .o-mail-Thread-jumpPresent { background-color: mix(map-get($theme-colors, 'info'), $o-webclient-background-color, 5%); z-index: $o-mail-NavigableList-zIndex - 2; diff --git a/addons/mail/static/src/core_ui/thread.xml b/addons/mail/static/src/core_ui/thread.xml index 55dff09a31a..addaf2f9662 100644 --- a/addons/mail/static/src/core_ui/thread.xml +++ b/addons/mail/static/src/core_ui/thread.xml @@ -115,9 +115,8 @@
+ }" t-ref="root">
diff --git a/addons/mail/static/src/discuss/discuss_patch.xml b/addons/mail/static/src/discuss/discuss_patch.xml index ca044ae1c1f..b109875dd94 100644 --- a/addons/mail/static/src/discuss/discuss_patch.xml +++ b/addons/mail/static/src/discuss/discuss_patch.xml @@ -12,7 +12,7 @@ - + diff --git a/addons/mail/static/src/discuss/message_pin/discuss_patch.js b/addons/mail/static/src/discuss/message_pin/discuss_patch.js new file mode 100644 index 00000000000..6a1e6e445e3 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/discuss_patch.js @@ -0,0 +1,32 @@ +/* @odoo-module */ + +import { PinnedMessagesPanel } from "@mail/discuss/message_pin/pinned_messages_panel"; +import { Discuss, MODES } from "@mail/discuss_app/discuss"; +import { useChildSubEnv } from "@odoo/owl"; +import { patch } from "@web/core/utils/patch"; + +MODES.PINNED_MESSAGES = "pinned-messages"; + +patch(Discuss, "discuss/message_pin", { + components: { ...Discuss.components, PinnedMessagesPanel }, +}); + +patch(Discuss.prototype, "discuss/message_pin", { + setup() { + this._super(); + useChildSubEnv({ + pinMenu: { + open: () => (this.state.activeMode = "pinned-messages"), + close: () => { + if (this.state.activeMode === "pinned-messages") { + this.state.activeMode = ""; + } + }, + }, + }); + }, + togglePinMenu() { + this.state.activeMode = + this.state.activeMode === MODES.PINNED_MESSAGES ? MODES.NONE : MODES.PINNED_MESSAGES; + }, +}); diff --git a/addons/mail/static/src/discuss/message_pin/discuss_patch.xml b/addons/mail/static/src/discuss/message_pin/discuss_patch.xml new file mode 100644 index 00000000000..9a963fe9a97 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/discuss_patch.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/addons/mail/static/src/discuss/message_pin/message_patch.js b/addons/mail/static/src/discuss/message_pin/message_patch.js new file mode 100644 index 00000000000..84d9a7cb770 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/message_patch.js @@ -0,0 +1,55 @@ +/* @odoo-module */ + +import { Message } from "@mail/core_ui/message"; +import { MessageConfirmDialog } from "@mail/core_ui/message_confirm_dialog"; +import { useMessagePinService } from "@mail/discuss/message_pin/message_pin_service"; +import { _t } from "@web/core/l10n/translation"; +import { patch } from "@web/core/utils/patch"; + +patch(Message, "discuss/message_pin", { + components: { ...Message.components }, +}); + +patch(Message.prototype, "discuss/message_pin", { + setup() { + this._super(); + this.messagePinService = useMessagePinService(); + }, + onClickPin() { + const prompt = this.messagePinService.getPinnedAt(this.message.id) + ? _t("Are you sure you want to remove this pinned message?") + : _t( + "The following message will be pinned to the channel. Are you sure you want to continue?" + ); + this.env.services.dialog.add(MessageConfirmDialog, { + message: this.message, + messageComponent: Message, + prompt, + onConfirm: () => + this.messagePinService.setPin( + this.message, + !this.messagePinService.getPinnedAt(this.message.id) + ), + }); + }, + get attClass() { + const res = this._super(); + return Object.assign(res, { + "o-cancelSelfAuthored": this.env.pinnedPanel, + "mt-1": res["mt-1"] && !this.env.pinnedPanel, + "px-3": res["px-3"] && !this.env.pinnedPanel, + }); + }, + get isAlignedRight() { + return !this.env.pinnedPanel && this._super(); + }, + get pinOptionText() { + return this.messagePinService.getPinnedAt(this.message.id) ? _t("Unpin") : _t("Pin"); + }, + get shouldDisplayAuthorName() { + if (this.env.pinnedPanel) { + return true; + } + return this._super(); + }, +}); diff --git a/addons/mail/static/src/discuss/message_pin/message_patch.xml b/addons/mail/static/src/discuss/message_pin/message_patch.xml new file mode 100644 index 00000000000..f6d2b25111d --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/message_patch.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/addons/mail/static/src/discuss/message_pin/message_pin_service.js b/addons/mail/static/src/discuss/message_pin/message_pin_service.js new file mode 100644 index 00000000000..ccfbf7158ca --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/message_pin_service.js @@ -0,0 +1,198 @@ +/* @odoo-module */ + +import { markup, reactive, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +export const OTHER_LONG_TYPING = 60000; + +export class MessagePin { + busService; + /** @type {import("@mail/core/message_service").MessageService} */ + messageService; + /** @type {Map} */ + loadStateByChannelId = new Map(); + /** @type {Map>} */ + messageIdsByChannelId = new Map(); + /** @type {Map} */ + channelIdByMessageId = new Map(); + /** @type {Map} */ + pinnedAtByMessageId = new Map(); + /** @type {import("@mail/core/store_service").Store} */ + storeService; + + constructor( + env, + { + bus_service: busService, + "mail.message": messageService, + "mail.store": storeService, + orm: ormService, + rpc: rpcService, + } + ) { + Object.assign(this, { + busService, + env, + messageService, + ormService, + rpcService, + storeService, + }); + } + + setup() { + this.env.bus.addEventListener("mail.message/onUpdate", ({ detail: { message, data } }) => { + this.onMessageUpdate(message, data); + }); + this.busService.subscribe("mail.message/delete", ({ message_ids }) => { + for (const messageId of message_ids) { + this.removePinnedMessage(messageId); + } + }); + this.busService.start(); + } + + /** + * @param {number} channelId + * @param {number} messageId + * @param {string} pinnedAt + */ + addPinnedMessage(channelId, messageId, pinnedAt) { + if (!this.messageIdsByChannelId.has(channelId)) { + this.messageIdsByChannelId.set(channelId, new Set()); + } + const messageIds = this.messageIdsByChannelId.get(channelId); + messageIds.add(messageId); + this.channelIdByMessageId.set(messageId, channelId); + this.pinnedAtByMessageId.set(messageId, pinnedAt); + } + + /** + * @param {import("@mail/core/thread_model").Thread} channel + */ + async fetchPinnedMessages(channel) { + if ( + channel.model !== "discuss.channel" || + ["loaded", "loading"].includes(this.loadStateByChannelId.get(channel.id)) + ) { + return; + } + this.loadStateByChannelId.set(channel.id, "loading"); + try { + const messagesData = await this.rpcService("/discuss/channel/pinned_messages", { + channel_id: channel.id, + }); + messagesData.forEach((messageData) => { + if (messageData.parentMessage) { + messageData.parentMessage.body = markup(messageData.parentMessage.body); + } + messageData.body = markup(messageData.body); + this.messageService.insert(messageData); + }); + this.loadStateByChannelId.set(channel.id, "loaded"); + } catch (e) { + this.loadStateByChannelId.set(channel.id, "error"); + throw e; + } + } + + /** + * @param {number} messageId + * @returns {string|null} + */ + getPinnedAt(messageId) { + const pinnedAt = this.pinnedAtByMessageId.get(messageId); + return pinnedAt ? luxon.DateTime.fromISO(new Date(pinnedAt).toISOString()) : null; + } + + /** + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {import("@mail/core/message_model").Message[]} + */ + getPinnedMessages(channel) { + return [...(this.messageIdsByChannelId.get(channel.id) ?? new Set())] + .map((id) => this.messageService.insert({ id })) + .sort((a, b) => { + const aPinnedAt = this.pinnedAtByMessageId.get(a.id); + const bPinnedAt = this.pinnedAtByMessageId.get(b.id); + if (aPinnedAt === bPinnedAt) { + return b.id - a.id; + } + return aPinnedAt < bPinnedAt ? 1 : -1; + }); + } + + /** + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {boolean} + */ + hasPinnedMessages(channel) { + return this.getPinnedMessages(channel).length > 0; + } + + /** + * @param {import("@mail/core/message_model").Message} + * @param {Object} data + */ + onMessageUpdate(message, { pinned_at: pinnedAt }) { + if ( + message.originThread?.model === "discuss.channel" && + (pinnedAt !== undefined || message.isEmpty) + ) { + if (pinnedAt && !message.isEmpty) { + this.addPinnedMessage(message.originThread.id, message.id, pinnedAt); + } else { + this.removePinnedMessage(message.id); + } + } + } + + /** + * @param {number} messageId + */ + removePinnedMessage(messageId) { + const channelId = this.channelIdByMessageId.get(messageId); + if (!channelId) { + return; + } + const messageIds = this.messageIdsByChannelId.get(channelId); + if (messageIds) { + messageIds.delete(messageId); + if (messageIds.size === 0) { + this.messageIdsByChannelId.delete(channelId); + } + } + this.channelIdByMessageId.delete(messageId); + this.pinnedAtByMessageId.delete(messageId); + } + + /** + * @param {import("@mail/core/message_model").Message} + * @param {boolean} pinned + */ + setPin(message, pinned) { + this.ormService.call("discuss.channel", "set_message_pin", [message.originThread.id], { + message_id: message.id, + pinned, + }); + } +} + +export const messagePinService = { + dependencies: ["bus_service", "mail.message", "mail.store", "orm", "rpc"], + start(env, services) { + const messagePin = reactive(new MessagePin(env, services)); + messagePin.setup(); + return messagePin; + }, +}; + +registry.category("services").add("discuss.message.pin", messagePinService); + +/** + * @returns {MessagePin} + * */ +export function useMessagePinService() { + return useState(useService("discuss.message.pin")); +} diff --git a/addons/mail/static/src/discuss_app/pinned_messages_panel.js b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.js similarity index 84% rename from addons/mail/static/src/discuss_app/pinned_messages_panel.js rename to addons/mail/static/src/discuss/message_pin/pinned_messages_panel.js index c98c09ad547..86ddec993c3 100644 --- a/addons/mail/static/src/discuss_app/pinned_messages_panel.js +++ b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.js @@ -1,17 +1,18 @@ /* @odoo-module */ -import { Component, onWillStart, onWillUpdateProps, useState, useSubEnv } from "@odoo/owl"; import { Message } from "@mail/core_ui/message"; import { MessageConfirmDialog } from "@mail/core_ui/message_confirm_dialog"; -import { useService } from "@web/core/utils/hooks"; +import { useMessagePinService } from "@mail/discuss/message_pin/message_pin_service"; +import { Component, onWillStart, onWillUpdateProps, useState, useSubEnv } from "@odoo/owl"; import { _t } from "@web/core/l10n/translation"; +import { useService } from "@web/core/utils/hooks"; export class PinnedMessagesPanel extends Component { static components = { Message, }; static props = ["thread"]; - static template = "mail.PinnedMessagesPanel"; + static template = "discuss.PinnedMessagesPanel"; setup() { this.threadService = useService("mail.thread"); @@ -19,13 +20,13 @@ export class PinnedMessagesPanel extends Component { this.store = useService("mail.store"); this.rpc = useService("rpc"); this.ui = useState(useService("ui")); - + this.messagePinService = useMessagePinService(); onWillStart(() => { - this.threadService.fetchPinnedMessages(this.props.thread); + this.messagePinService.fetchPinnedMessages(this.props.thread); }); onWillUpdateProps(async (nextProps) => { if (nextProps.thread.id !== this.props.thread.id) { - this.threadService.fetchPinnedMessages(nextProps.thread); + this.messagePinService.fetchPinnedMessages(nextProps.thread); } }); useSubEnv({ @@ -60,7 +61,7 @@ export class PinnedMessagesPanel extends Component { message, messageComponent: Message, prompt: _t("Are you sure you want to remove this pinned message?"), - onConfirm: () => this.messageService.setPin(message, false), + onConfirm: () => this.messagePinService.setPin(message, false), }); } diff --git a/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.scss b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.scss new file mode 100644 index 00000000000..6deb2fb7ef1 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.scss @@ -0,0 +1,19 @@ +.o-discuss-PinnedMessagesPanel { + flex-basis: 425px; +} + +.o-discuss-PinnedMessagesPanel-title { + z-index: 1; +} + +.o-discuss-PinnedMessagesPanel-jump { + color: white; +} + +.card-body:hover .o-discuss-PinnedMessagesPanel-jump { + opacity: 100 !important; +} + +.o_touch_device .o-discuss-PinnedMessagesPanel-jump.btn { + font-size: 0.9rem; +} diff --git a/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.xml b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.xml new file mode 100644 index 00000000000..0e354a9b2a2 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/pinned_messages_panel.xml @@ -0,0 +1,26 @@ + + + +
+

+ Pinned messages +

+
+
+
+
+ + +
+ +
+
+

+

+
+
+
diff --git a/addons/mail/static/src/discuss/message_pin/thread_patch.js b/addons/mail/static/src/discuss/message_pin/thread_patch.js new file mode 100644 index 00000000000..ff88052d070 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/thread_patch.js @@ -0,0 +1,23 @@ +/* @odoo-module */ + +import { Thread } from "@mail/core_ui/thread"; +import { useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; + +patch(Thread.prototype, "discuss/message_pin", { + setup() { + this._super(); + this.ui = useState(useService("ui")); + }, + /** + * @override + * @param {MouseEvent} ev + */ + async onClickNotification(ev) { + const { oeType } = ev.target.dataset; + if (oeType === "pin-menu") { + this.env.pinMenu?.open(); + } + }, +}); diff --git a/addons/mail/static/src/discuss/message_pin/thread_patch.scss b/addons/mail/static/src/discuss/message_pin/thread_patch.scss new file mode 100644 index 00000000000..f860fcc0d30 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/thread_patch.scss @@ -0,0 +1,3 @@ +.o-mail-Thread-noPinnedPanel [data-oe-type="pin-menu"] { + display: none; +} diff --git a/addons/mail/static/src/discuss/message_pin/thread_patch.xml b/addons/mail/static/src/discuss/message_pin/thread_patch.xml new file mode 100644 index 00000000000..1f8ccc5c1d4 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/thread_patch.xml @@ -0,0 +1,8 @@ + + + + + {{ env.inChatWindow and !ui.isSmall ? 'o-mail-Thread-noPinnedPanel' : '' }} + + + diff --git a/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.js b/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.js new file mode 100644 index 00000000000..8efd73d9198 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.js @@ -0,0 +1,32 @@ +/* @odoo-module */ + +import { PinnedMessagesPanel } from "@mail/discuss/message_pin/pinned_messages_panel"; +import { ChatWindow, MODES } from "@mail/web/chat_window/chat_window"; +import { useChildSubEnv } from "@odoo/owl"; +import { patch } from "@web/core/utils/patch"; + +MODES.PINNED_MESSAGES = "pinned-messages"; + +patch(ChatWindow, "discuss/message_pin", { + components: { ...ChatWindow.components, PinnedMessagesPanel }, +}); + +patch(ChatWindow.prototype, "discuss/message_pin", { + setup() { + this._super(); + useChildSubEnv({ + pinMenu: { + open: () => (this.state.activeMode = MODES.PINNED_MESSAGES), + close: () => { + if (this.state.activeMode === MODES.PINNED_MESSAGES) { + this.state.activeMode = MODES.NONE; + } + }, + }, + }); + }, + togglePinMenu() { + this.state.activeMode = + this.state.activeMode === MODES.PINNED_MESSAGES ? MODES.NONE : MODES.PINNED_MESSAGES; + }, +}); diff --git a/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.xml b/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.xml new file mode 100644 index 00000000000..06fd1b000c9 --- /dev/null +++ b/addons/mail/static/src/discuss/message_pin/web/chat_window_patch.xml @@ -0,0 +1,13 @@ + + + + +
+ +
+
+ + + +
+
diff --git a/addons/mail/static/src/discuss/web/chat_window_patch.xml b/addons/mail/static/src/discuss/web/chat_window_patch.xml index a772a391aba..553fd45776f 100644 --- a/addons/mail/static/src/discuss/web/chat_window_patch.xml +++ b/addons/mail/static/src/discuss/web/chat_window_patch.xml @@ -14,10 +14,10 @@
- - - - + + + +
diff --git a/addons/mail/static/src/discuss_app/discuss.js b/addons/mail/static/src/discuss_app/discuss.js index 30a4d92376f..b0689a76d09 100644 --- a/addons/mail/static/src/discuss_app/discuss.js +++ b/addons/mail/static/src/discuss_app/discuss.js @@ -23,7 +23,12 @@ import { import { useService } from "@web/core/utils/hooks"; import { _t } from "@web/core/l10n/translation"; import { url } from "@web/core/utils/urls"; -import { PinnedMessagesPanel } from "./pinned_messages_panel"; + +export const MODES = { + MEMBER_LIST: "member-list", + SETTINGS: "settings", + NONE: "", +}; export class Discuss extends Component { static components = { @@ -32,7 +37,6 @@ export class Discuss extends Component { ThreadIcon, Composer, Call, - PinnedMessagesPanel, FileUploader, ImStatus, }; @@ -41,14 +45,8 @@ export class Discuss extends Component { }; static template = "mail.Discuss"; - MODES = Object.freeze({ - MEMBER_LIST: "member-list", - PINNED_MESSAGES: "pinned-messages", - SETTINGS: "settings", - NONE: "", - }); - setup() { + this.MODES = MODES; this.messaging = useMessaging(); this.store = useStore(); this.threadService = useState(useService("mail.thread")); @@ -59,7 +57,7 @@ export class Discuss extends Component { this.messageEdition = useMessageEdition(); this.messageToReplyTo = useMessageToReplyTo(); this.contentRef = useRef("content"); - this.state = useState({ activeMode: this.MODES.NONE }); + this.state = useState({ activeMode: MODES.NONE }); this.orm = useService("orm"); this.effect = useService("effect"); this.ui = useState(useService("ui")); @@ -67,14 +65,6 @@ export class Discuss extends Component { useChildSubEnv({ inDiscussApp: true, messageHighlight: this.messageHighlight, - pinMenu: { - open: () => (this.state.activeMode = this.MODES.PINNED_MESSAGES), - close: () => { - if (this.state.activeMode === this.MODES.PINNED_MESSAGES) { - this.state.activeMode = this.MODES.NONE; - } - }, - }, }); this.notification = useService("notification"); useEffect( @@ -115,13 +105,6 @@ export class Discuss extends Component { : this.thread.imgUrl; } - togglePinMenu() { - this.state.activeMode = - this.state.activeMode === this.MODES.PINNED_MESSAGES - ? this.MODES.NONE - : this.MODES.PINNED_MESSAGES; - } - async onFileUploaded(file) { await this.threadService.notifyThreadAvatarToServer(this.thread.id, file.data); this.notification.add(_t("The avatar has been updated!"), { type: "success" }); diff --git a/addons/mail/static/src/discuss_app/discuss.xml b/addons/mail/static/src/discuss_app/discuss.xml index c07658a1c6f..e5c1fffbf46 100644 --- a/addons/mail/static/src/discuss_app/discuss.xml +++ b/addons/mail/static/src/discuss_app/discuss.xml @@ -42,9 +42,6 @@
- @@ -63,13 +60,12 @@
-
+
-
diff --git a/addons/mail/static/src/discuss_app/pinned_messages_panel.scss b/addons/mail/static/src/discuss_app/pinned_messages_panel.scss deleted file mode 100644 index a424f21f4de..00000000000 --- a/addons/mail/static/src/discuss_app/pinned_messages_panel.scss +++ /dev/null @@ -1,19 +0,0 @@ -.o-mail-PinnedMessagesPanel { - flex-basis: 425px; -} - -.o-mail-PinnedMessagesPanel-title { - z-index: 1; -} - -.o-mail-PinnedMessagesPanel-jump { - color: white; -} - -.card-body:hover .o-mail-PinnedMessagesPanel-jump { - opacity: 100 !important; -} - -.o_touch_device .o-mail-PinnedMessagesPanel-jump.btn { - font-size: 0.9rem; -} diff --git a/addons/mail/static/src/discuss_app/pinned_messages_panel.xml b/addons/mail/static/src/discuss_app/pinned_messages_panel.xml deleted file mode 100644 index 2d7d18a47c7..00000000000 --- a/addons/mail/static/src/discuss_app/pinned_messages_panel.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - -
-

- Pinned messages -

-
-
-
-
- - -
- -
-
-

-

-
-
-
diff --git a/addons/mail/static/src/web/chat_window/chat_window.js b/addons/mail/static/src/web/chat_window/chat_window.js index 34af53d7caf..7d90cf0c9cd 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.js +++ b/addons/mail/static/src/web/chat_window/chat_window.js @@ -13,9 +13,12 @@ import { ThreadIcon } from "@mail/discuss_app/thread_icon"; import { ImStatus } from "@mail/discuss_app/im_status"; import { isEventHandled } from "@mail/utils/misc"; import { ChannelSelector } from "@mail/discuss_app/channel_selector"; -import { PinnedMessagesPanel } from "@mail/discuss_app/pinned_messages_panel"; import { _t } from "@web/core/l10n/translation"; +export const MODES = { + NONE: "", +}; + /** * @typedef {Object} Props * @property {import("@mail/web/chat_window/chat_window_model").ChatWindow} chatWindow @@ -30,12 +33,12 @@ export class ChatWindow extends Component { Composer, ThreadIcon, ImStatus, - PinnedMessagesPanel, }; static props = ["chatWindow", "right?"]; static template = "mail.ChatWindow"; setup() { + this.MODES = MODES; this.store = useStore(); /** @type {import("@mail/web/chat_window/chat_window_service").ChatWindowService} */ this.chatWindowService = useState(useService("mail.chat_window")); @@ -48,13 +51,12 @@ export class ChatWindow extends Component { this.state = useState({ /** * activeMode: - * "pinned-messages": pin menu is displayed * "member-list": channel member list is displayed * "in-settings": settings is displayed * "add-users": add users is displayed (small device) * "": no action pannel */ - activeMode: "", + activeMode: MODES.NONE, }); this.action = useService("action"); this.ui = useState(useService("ui")); @@ -62,14 +64,6 @@ export class ChatWindow extends Component { useChildSubEnv({ inChatWindow: true, messageHighlight: this.messageHighlight, - pinMenu: { - open: () => (this.state.activeMode = "pinned-messages"), - close: () => { - if (this.state.activeMode === "pinned-messages") { - this.state.activeMode = ""; - } - }, - }, }); } @@ -124,11 +118,6 @@ export class ChatWindow extends Component { this.chatWindowService.notifyState(this.props.chatWindow); } - togglePinMenu() { - this.state.activeMode = - this.state.activeMode === "pinned-messages" ? "" : "pinned-messages"; - } - expand() { if (this.thread.type === "chatter") { this.action.doAction({ diff --git a/addons/mail/static/src/web/chat_window/chat_window.xml b/addons/mail/static/src/web/chat_window/chat_window.xml index 6cef10aac89..3a0cd757dd6 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.xml +++ b/addons/mail/static/src/web/chat_window/chat_window.xml @@ -32,9 +32,6 @@ ()
-
- -
@@ -48,9 +45,8 @@
- - - + + diff --git a/addons/mail/static/tests/discuss_app/pinned_messages_tests.js b/addons/mail/static/tests/discuss/message_pin/pinned_messages_tests.js similarity index 87% rename from addons/mail/static/tests/discuss_app/pinned_messages_tests.js rename to addons/mail/static/tests/discuss/message_pin/pinned_messages_tests.js index 7d5dae9382b..7e173376289 100644 --- a/addons/mail/static/tests/discuss_app/pinned_messages_tests.js +++ b/addons/mail/static/tests/discuss/message_pin/pinned_messages_tests.js @@ -18,12 +18,12 @@ QUnit.test("Pin message", async (assert) => { await click(".o-mail-Discuss-header button[title='Pinned messages']"); assert.containsOnce( $, - ".o-mail-PinnedMessagesPanel:contains(This channel doesn't have any pinned messages.)" + ".o-discuss-PinnedMessagesPanel:contains(This channel doesn't have any pinned messages.)" ); await click(".o-mail-Message [title='Expand']"); await click(".dropdown-item:contains(Pin)"); await click(".modal-footer button:contains(Confirm)"); - assert.containsOnce($, ".o-mail-PinnedMessagesPanel .o-mail-Message:contains(Hello world)"); + assert.containsOnce($, ".o-discuss-PinnedMessagesPanel .o-mail-Message:contains(Hello world)"); }); QUnit.test("Unpin message", async (assert) => { @@ -38,11 +38,11 @@ QUnit.test("Unpin message", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await click(".o-mail-Discuss-header button[title='Pinned messages']"); - assert.containsOnce($, ".o-mail-PinnedMessagesPanel .o-mail-Message"); + assert.containsOnce($, ".o-discuss-PinnedMessagesPanel .o-mail-Message"); await click(".o-mail-Message [title='Expand']"); await click(".dropdown-item:contains(Unpin)"); await click(".modal-footer button:contains(Confirm)"); - assert.containsNone($, ".o-mail-PinnedMessagesPanel .o-mail-Message"); + assert.containsNone($, ".o-discuss-PinnedMessagesPanel .o-mail-Message"); }); QUnit.test("Deleted messages are not pinned", async (assert) => { @@ -58,11 +58,11 @@ QUnit.test("Deleted messages are not pinned", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await click(".o-mail-Discuss-header button[title='Pinned messages']"); - assert.containsOnce($, ".o-mail-PinnedMessagesPanel .o-mail-Message"); + assert.containsOnce($, ".o-discuss-PinnedMessagesPanel .o-mail-Message"); await click(".o-mail-Message [title='Expand']"); await click(".dropdown-item:contains(Delete)"); await click("button:contains(Confirm)"); - assert.containsNone($, ".o-mail-PinnedMessagesPanel .o-mail-Message"); + assert.containsNone($, ".o-discuss-PinnedMessagesPanel .o-mail-Message"); }); QUnit.test("Open pinned panel from notification", async (assert) => { @@ -78,9 +78,9 @@ QUnit.test("Open pinned panel from notification", async (assert) => { await click(".o-mail-Message:eq(0) [title='Expand']"); await click(".dropdown-item:contains(Pin)"); await click(".modal-footer button:contains(Confirm)"); - assert.containsNone($, ".o-mail-PinnedMessagesPanel"); + assert.containsNone($, ".o-discuss-PinnedMessagesPanel"); await click(".o_mail_notification a:contains(See all pinned messages)"); - assert.containsOnce($, ".o-mail-PinnedMessagesPanel"); + assert.containsOnce($, ".o-discuss-PinnedMessagesPanel"); }); QUnit.test("Jump to message", async (assert) => { @@ -109,7 +109,7 @@ QUnit.test("Jump to message", async (assert) => { const { openDiscuss } = await start(); await openDiscuss(channelId); await click(".o-mail-Discuss-header button[title='Pinned messages']"); - await click(".o-mail-PinnedMessagesPanel button:contains(Jump)"); + await click(".o-discuss-PinnedMessagesPanel button:contains(Jump)"); await nextTick(); assert.isVisible($(".o-mail-Message:contains(Hello world!)")); }); diff --git a/addons/mail/static/tests/helpers/discuss/message_pin/setup_manager_patch.js b/addons/mail/static/tests/helpers/discuss/message_pin/setup_manager_patch.js new file mode 100644 index 00000000000..eada1b2478e --- /dev/null +++ b/addons/mail/static/tests/helpers/discuss/message_pin/setup_manager_patch.js @@ -0,0 +1,14 @@ +/* @odoo-module */ + +import { messagePinService } from "@mail/discuss/message_pin/message_pin_service"; +import { setupManager } from "@mail/../tests/helpers/webclient_setup"; +import { patch } from "@web/core/utils/patch"; + +patch(setupManager, "discuss/message_pin", { + setupServices() { + return { + ...this._super(...arguments), + "discuss.message.pin": messagePinService, + }; + }, +});