[REF] mail: move message pin to discuss folder

Part of task-3265211

closes odoo/odoo#121418

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2023-05-23 18:01:40 +02:00
parent 08eb87a982
commit 80de5298b2
36 changed files with 519 additions and 235 deletions
+2 -2
View File
@@ -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',
+1
View File
@@ -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,
}
})
@@ -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")
+1 -13
View File
@@ -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) {
@@ -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) {
@@ -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} */
@@ -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";
+22 -22
View File
@@ -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) {
+1 -1
View File
@@ -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 {
+1 -13
View File
@@ -4,18 +4,7 @@
<t t-name="mail.Message" owl="1">
<ActionSwiper onRightSwipe="hasTouch() and isInInbox ? { action: () => this.messageService.setDone(this.message), bgColor: 'bg-success', icon: 'fa-check-circle' } : undefined">
<div class="o-mail-Message position-relative py-1"
t-att-class="{
'o-pinnedPanel': env.pinnedPanel,
'o-highlighted-from-mention': message.isHighlightedFromMention,
'o-highlighted bg-view shadow-lg': props.highlighted,
'o-selfAuthored': message.isSelfAuthored,
'o-selected': props.messageToReplyTo?.isSelected(props.thread, props.message),
'o-squashed pt-1': props.squashed,
'mt-1': !props.squashed and props.thread and !env.pinnedPanel,
'px-3': !props.isInChatWindow and !env.pinnedPanel,
'px-1': props.isInChatWindow,
'opacity-50': props.messageToReplyTo?.isNotSelected(props.thread, props.message),
}"
t-att-class="attClass"
role="group"
t-att-aria-label="messageTypeText"
t-on-click="onClick"
@@ -145,7 +134,6 @@
<DropdownItem t-if="message.reactions.length" class="'px-2 d-flex align-items-center rounded-0'" title="'Reactions'" onSelected="() => this.openReactionMenu()"><i class="fa fa-lg fa-smile-o pe-2"/>View Reactions</DropdownItem>
<DropdownItem t-if="isInInbox" class="'px-2 d-flex align-items-center rounded-0'" title="'Mark as Read'" onSelected="() => messageService.setDone(props.message)"><i class="fa fa-lg fa-check pe-2"/>Mark as Read</DropdownItem>
<DropdownItem t-if="canUnfollow" class="'px-2 d-flex align-items-center rounded-0'" title="'Unfollow'" onSelected="() => messageService.unfollow(props.message)"><i class="fa fa-lg fa-user-times pe-2"/>Unfollow</DropdownItem>
<DropdownItem t-if="store.user and props.thread.model === 'discuss.channel'" class="'px-2 d-flex align-items-center rounded-0'" title="pinOptionText" onSelected="() => this.onClickPin()"><i class="fa fa-lg fa-thumb-tack pe-2"/><t t-esc="pinOptionText"/></DropdownItem>
<DropdownItem t-if="props.thread.model === 'discuss.channel'" class="'px-2 d-flex align-items-center rounded-0'" title="'Mark as Unread'" onSelected="() => this.onClickMarkAsUnread()"><i class="fa fa-lg fa-eye-slash pe-2"/>Mark as Unread</DropdownItem>
<DropdownItem t-if="editable" class="'px-2 d-flex align-items-center rounded-0'" title="'Edit'" onSelected="() => this.onClickEdit()"><i class="fa fa-lg fa-pencil pe-2"/>Edit</DropdownItem>
<DropdownItem t-if="deletable" class="'px-2 d-flex align-items-center rounded-0'" title="'Delete'" onSelected="() => this.onClickDelete()"><i class="fa fa-lg fa-trash pe-2"/>Delete</DropdownItem>
-3
View File
@@ -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();
}
}
@@ -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;
+1 -2
View File
@@ -115,9 +115,8 @@
<t t-name="mail.NotificationMessage" owl="1">
<div class="o-mail-NotificationMessage mx-auto text-500 small px-3 text-center" t-on-click="onClickNotification" t-att-class="{
'o-mail-Thread-noPinnedPanel': env.inChatWindow and !ui.isSmall,
'mt-2': prevMsg and !prevMsg.isNotification,
}">
}" t-ref="root">
<i t-if="msg.notificationIcon" t-attf-class="{{ msg.notificationIcon }} me-1"/>
<span t-if="!msg.body.includes(escape(msg.author.name))" class="d-inline mx-1" t-esc="msg.author.name"/> <t t-out="msg.body"/>
</div>
@@ -12,7 +12,7 @@
<i class="fa fa-lg fa-gear text-700"/>
</button>
</xpath>
<xpath expr="//PinnedMessagesPanel" position="after">
<xpath expr="//*[@t-ref='core']" position="inside">
<ChannelMemberList t-if="thread.hasMemberList and state.activeMode === MODES.MEMBER_LIST" thread="thread" className="'o-mail-Discuss-channelMemberList flex-shrink-0 border-start'"/>
<CallSettings t-if="thread.allowCalls and state.activeMode === MODES.SETTINGS" thread="thread" className="'o-mail-Discuss-callSettings border-start'"/>
</xpath>
@@ -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;
},
});
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Discuss" t-inherit-mode="extension">
<xpath expr="//*[@t-ref='startCall']" position="before">
<button t-if="thread.model === 'discuss.channel'" class="btn rounded px-2 shadow-none" t-att-class="{ 'o-isActive': state.activeMode === MODES.PINNED_MESSAGES }" title="Pinned messages" t-on-click.stop="togglePinMenu">
<i class="fa fa-lg fa-thumb-tack text-700"/>
</button>
</xpath>
<xpath expr="//*[@t-ref='core']" position="inside">
<PinnedMessagesPanel t-if="state.activeMode === MODES.PINNED_MESSAGES and thread.model === 'discuss.channel'" thread="thread"/>
</xpath>
</t>
</templates>
@@ -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();
},
});
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Message" t-inherit-mode="extension">
<xpath expr="//DropdownItem[@t-if='canUnfollow']" position="after">
<DropdownItem
t-if="store.user and props.thread.model === 'discuss.channel'"
class="'px-2 d-flex align-items-center rounded-0'"
title="pinOptionText"
onSelected="() => this.onClickPin()"
>
<span><i class="fa fa-lg fa-thumb-tack pe-2"/><t t-esc="pinOptionText"/></span>
</DropdownItem>
</xpath>
</t>
</templates>
@@ -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<number, string>} */
loadStateByChannelId = new Map();
/** @type {Map<number, Set<number>>} */
messageIdsByChannelId = new Map();
/** @type {Map<number, number>} */
channelIdByMessageId = new Map();
/** @type {Map<number, string>} */
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"));
}
@@ -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),
});
}
@@ -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;
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="discuss.PinnedMessagesPanel" owl="1">
<div class="o-discuss-PinnedMessagesPanel overflow-auto d-flex flex-column flex-shrink-0 bg-view position-relative border-start" t-att-class="{
'flex-grow-1': env.inChatWindow,
}">
<p class="o-discuss-PinnedMessagesPanel-title position-sticky top-0 py-3 mb-0 fw-bolder text-center text-uppercase text-700">
Pinned messages
</p>
<div class="d-flex flex-column" t-att-class="{ 'justify-content-center flex-grow-1': !messagePinService.hasPinnedMessages(props.thread) }" t-ref="message-list">
<div class="card m-3 mt-0" t-foreach="messagePinService.getPinnedMessages(props.thread)" t-as="message" t-key="message.id">
<div class="card-body">
<div class="d-flex justify-content-end">
<button class="o-discuss-PinnedMessagesPanel-jump btn rounded bg-400 badge opacity-0 flex-shrink-0" t-att-class="{ 'opacity-100 py-1 px-2': ui.isSmall }" t-on-click="() => this.onClickJump(message)">Jump</button>
<button class="btn ms-2 p-0 text-700 shadow-none" t-att-class="{ 'fs-5 align-self-end': ui.isSmall }" title="Unpin" t-on-click="() => this.onClickUnpin(message)">
<i class="d-block fa fa-times"/>
</button>
</div>
<Message hasActions="false" message="message" thread="props.thread"/>
</div>
</div>
<p t-if="!messagePinService.hasPinnedMessages(props.thread)" t-esc="emptyMessage" class="text-center fst-italic text-500 fs-6"/>
</div>
</div>
</t>
</templates>
@@ -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();
}
},
});
@@ -0,0 +1,3 @@
.o-mail-Thread-noPinnedPanel [data-oe-type="pin-menu"] {
display: none;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.NotificationMessage" t-inherit-mode="extension">
<xpath expr="//*[@t-ref='root']" position="attributes">
<attribute name="t-attf-class">{{ env.inChatWindow and !ui.isSmall ? 'o-mail-Thread-noPinnedPanel' : '' }}</>
</xpath>
</t>
</templates>
@@ -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;
},
});
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ChatWindow" t-inherit-mode="extension">
<xpath expr="//*[@t-ref='startCall']" position="before">
<div t-if="(thread?.model === 'discuss.channel' and ui.isSmall) or state.activeMode === MODES.PINNED_MESSAGES" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Pinned messages" t-on-click.stop="togglePinMenu">
<i class="fa fa-lg fa-thumb-tack"/>
</div>
</xpath>
<xpath expr="//t[@name='thread content']" position="inside">
<PinnedMessagesPanel t-if="state.activeMode === MODES.PINNED_MESSAGES" thread="thread"/>
</xpath>
</t>
</templates>
@@ -14,10 +14,10 @@
<i class="fa fa-gear"/>
</div>
</xpath>
<xpath expr="//PinnedMessagesPanel" position="after">
<CallSettings t-elif="state.activeMode === 'in-settings'" thread="thread" className="'flex-grow-1'"/>
<ChannelMemberList t-elif="state.activeMode === 'member-list'" thread="thread" className="'flex-shrink-0 border-start w-100 h-100'"/>
<ChannelInvitation t-elif="state.activeMode === 'add-users'" thread="thread" chatState="state"/>
<xpath expr="//t[@name='thread content']" position="inside">
<CallSettings t-if="state.activeMode === 'in-settings'" thread="thread" className="'flex-grow-1'"/>
<ChannelMemberList t-if="state.activeMode === 'member-list'" thread="thread" className="'flex-shrink-0 border-start w-100 h-100'"/>
<ChannelInvitation t-if="state.activeMode === 'add-users'" thread="thread" chatState="state"/>
</xpath>
</t>
</templates>
+8 -25
View File
@@ -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" });
@@ -42,9 +42,6 @@
<div class="o-mail-Discuss-header flex-shrink-0 d-flex align-items-center ms-1">
<button t-if="thread.id === 'inbox'" class="btn btn-link" t-att-disabled="thread.isEmpty" t-on-click="markAllAsRead">Mark all read</button>
<button t-if="thread.id === 'starred'" class="btn btn-link" t-att-disabled="thread.isEmpty" t-on-click="messageService.unstarAll">Unstar all</button>
<button t-if="thread.model === 'discuss.channel'" class="btn rounded px-2 shadow-none" t-att-class="{ 'o-isActive': state.activeMode === MODES.PINNED_MESSAGES }" title="Pinned messages" t-on-click.stop="togglePinMenu">
<i class="fa fa-lg fa-thumb-tack text-700"/>
</button>
<button t-if="thread.allowCalls and thread.type !== 'mailbox' and thread !== rtc.state.channel" class="btn rounded px-2 shadow-none" title="Start a Call" t-on-click="() => this.rtc.toggleCall(this.thread)" t-ref="startCall">
<i class="fa fa-lg fa-phone text-700"/>
</button>
@@ -63,13 +60,12 @@
</div>
</div>
</div>
<div class="overflow-auto bg-view d-flex flex-grow-1">
<div class="overflow-auto bg-view d-flex flex-grow-1" t-ref="core">
<div class="d-flex flex-column flex-grow-1">
<Call t-if="Object.keys(thread.rtcSessions).length > 0" thread="thread"/>
<Thread thread="thread" t-key="thread.localId" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo"/>
<Composer t-if="thread.type !== 'mailbox' or messageToReplyTo.thread === thread" t-key="thread.localId" composer="thread.composer" autofocus="true" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" onDiscardCallback="() => messageToReplyTo.cancel()" dropzoneRef="contentRef"/>
</div>
<PinnedMessagesPanel t-if="state.activeMode === MODES.PINNED_MESSAGES and thread.model === 'discuss.channel'" thread="thread"/>
</div>
</div>
<div t-if="!thread and (!ui.isSmall or store.discuss.activeTab === 'mailbox')" class="d-flex flex-grow-1 align-items-center justify-content-center w-100 bg-view">
@@ -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;
}
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.PinnedMessagesPanel" owl="1">
<div class="o-mail-PinnedMessagesPanel overflow-auto d-flex flex-column flex-shrink-0 bg-view position-relative border-start" t-att-class="{
'flex-grow-1': env.inChatWindow,
}">
<p class="o-mail-PinnedMessagesPanel-title position-sticky top-0 py-3 mb-0 fw-bolder text-center text-uppercase text-700">
Pinned messages
</p>
<div class="d-flex flex-column" t-att-class="{ 'justify-content-center flex-grow-1': props.thread.pinnedMessages.length === 0 }" t-ref="message-list">
<div class="card m-3 mt-0" t-foreach="props.thread.pinnedMessages" t-as="message" t-key="message.id">
<div class="card-body">
<div class="d-flex justify-content-end">
<button class="o-mail-PinnedMessagesPanel-jump btn rounded bg-400 badge opacity-0 flex-shrink-0" t-att-class="{ 'opacity-100 py-1 px-2': ui.isSmall }" t-on-click="() => this.onClickJump(message)">Jump</button>
<button class="btn ms-2 p-0 text-700 shadow-none" t-att-class="{ 'fs-5 align-self-end': ui.isSmall }" title="Unpin"
t-on-click="() => this.onClickUnpin(message)">
<i class="d-block fa fa-times"/>
</button>
</div>
<Message hasActions="false" message="message" thread="props.thread"/>
</div>
</div>
<p t-if="props.thread.pinnedMessages.length === 0" t-esc="emptyMessage" class="text-center fst-italic text-500 fs-6"/>
</div>
</div>
</t>
</templates>
@@ -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({
@@ -32,9 +32,6 @@
(<t t-out="thread.message_unread_counter"/>)
</div>
<div class="flex-grow-1"/>
<div t-if="thread?.model === 'discuss.channel' and ui.isSmall" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Pinned messages" t-on-click.stop="togglePinMenu">
<i class="fa fa-lg fa-thumb-tack"/>
</div>
<div t-if="thread and thread.allowCalls and thread !== rtc.state.channel and !props.chatWindow.hidden" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Start a Call" t-on-click.stop="() => this.rtc.toggleCall(this.props.chatWindow.thread)" t-ref="startCall">
<i class="fa fa-phone"/>
</div>
@@ -48,9 +45,8 @@
</div>
</div>
<div t-if="!props.chatWindow.folded" class="o-mail-ChatWindow-content bg-view d-flex flex-column h-100 overflow-auto border-start border-end" t-ref="content">
<t t-if="thread">
<PinnedMessagesPanel t-if="state.activeMode === 'pinned-messages'" thread="thread"/>
<t t-else="">
<t t-if="thread" name="thread content">
<t t-if="state.activeMode === MODES.NONE">
<Call t-if="Object.keys(thread.rtcSessions).length > 0" thread="thread" compact="true"/>
<Thread isInChatWindow="true" thread="thread" t-key="thread.localId" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo"/>
<Composer composer="thread.composer" autofocus="props.chatWindow.autofocus" mode="'compact'" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" dropzoneRef="contentRef"/>
@@ -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!)"));
});
@@ -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,
};
},
});