[REF] mail: make thread actions more generic

This will remove the necessity to patch component, and remove
duplciation between discuss and chat window.

Part of task-3265211

closes odoo/odoo#123454

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2023-06-06 11:45:04 +02:00
parent 27865152bb
commit 08a06c6e8f
35 changed files with 390 additions and 403 deletions
@@ -2,7 +2,6 @@
import { ChatWindow } from "@mail/chat_window/chat_window";
import { FeedbackPanel } from "../feedback_panel/feedback_panel";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
import { SESSION_STATE } from "../core/livechat_service";
@@ -33,22 +32,4 @@ patch(ChatWindow.prototype, "im_livechat", {
this.livechatService.leaveSession();
this.chatbotService.stop();
},
get actions() {
if (!this.chatbotService.canRestart) {
return this._super();
}
return this._super().concat([
{
id: "restart",
name: _t("Restart Conversation"),
icon: "fa fa-fw fa-refresh",
onSelect: () => {
this.chatbotService.restart();
this.chatWindowService.show(this.props.chatWindow);
},
sequence: 99,
},
]);
},
});
@@ -0,0 +1,17 @@
/* @odoo-module */
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
registry.category("mail.thread/actions").add("restart", {
condition(component) {
return component.chatbotService.canRestart;
},
icon: "fa fa-fw fa-refresh",
name: _t("Restart Conversation"),
open(component) {
component.chatbotService.restart();
component.chatWindowService.show(component.props.chatWindow);
},
sequence: 99,
});
@@ -4,6 +4,7 @@ import { Call } from "@mail/rtc/call";
import { Thread } from "@mail/core_ui/thread";
import { Composer } from "@mail/composer/composer";
import { useStore } from "@mail/core/messaging_hook";
import { useThreadActions } from "@mail/core/thread_actions";
import { useRtc } from "@mail/rtc/rtc_hook";
import { useMessageEdition, useMessageHighlight, useMessageToReplyTo } from "@mail/utils/hooks";
import { Component, useChildSubEnv, useRef, useState } from "@odoo/owl";
@@ -16,10 +17,6 @@ import { _t } from "@web/core/l10n/translation";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
export const MODES = {
NONE: "",
};
/**
* @typedef {Object} Props
* @property {import("@mail/chat_window/chat_window_model").ChatWindow} chatWindow
@@ -40,7 +37,6 @@ export class ChatWindow extends Component {
static template = "mail.ChatWindow";
setup() {
this.MODES = MODES;
this.store = useStore();
/** @type {import("@mail/chat_window/chat_window_service").ChatWindowService} */
this.chatWindowService = useState(useService("mail.chat_window"));
@@ -50,19 +46,10 @@ export class ChatWindow extends Component {
this.messageEdition = useMessageEdition();
this.messageHighlight = useMessageHighlight();
this.messageToReplyTo = useMessageToReplyTo();
this.state = useState({
moreActionsExpanded: false,
/**
* activeMode:
* "member-list": channel member list is displayed
* "in-settings": settings is displayed
* "add-users": add users is displayed (small device)
* "": no action pannel
*/
activeMode: MODES.NONE,
});
this.state = useState({ moreActionsExpanded: false });
this.ui = useState(useService("ui"));
this.contentRef = useRef("content");
this.threadActions = useThreadActions();
useChildSubEnv({
inChatWindow: true,
messageHighlight: this.messageHighlight,
@@ -125,35 +112,6 @@ export class ChatWindow extends Component {
this.chatWindowService.notifyState(this.props.chatWindow);
}
get orderedActions() {
return this.actions.sort((act1, act2) => act1.sequence - act2.sequence);
}
get actions() {
const acts = [];
if (
this.thread?.allowCalls &&
this.thread !== this.rtc.state.channel &&
!this.props.chatWindow.hidden
) {
acts.push({
id: "call",
name: _t("Start a Call"),
icon: "fa fa-fw fa-phone",
onSelect: () => this.rtc.toggleCall(this.props.chatWindow.thread),
sequence: 10,
});
}
acts.push({
id: "close",
name: _t("Close Chat Window"),
icon: "fa fa-fw fa-close",
onSelect: () => this.close(),
sequence: 100,
});
return acts;
}
get moreMenuText() {
return _t("More actions");
}
@@ -32,33 +32,34 @@
(<t t-out="thread.message_unread_counter"/>)
</div>
<div class="flex-grow-1"/>
<t t-if="actions.length > 1">
<t t-set="firstAction" t-value="orderedActions[0]"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 opacity-75 opacity-100-hover" t-att-title="firstAction.name" t-on-click.stop="firstAction.onSelect"><i t-att-class="firstAction.icon"/></button>
<t t-if="threadActions.actions.length > 1">
<t t-set="firstAction" t-value="threadActions.actions[0]"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 opacity-75 opacity-100-hover" t-att-title="firstAction.name" t-on-click.stop="() => firstAction.onSelect()"><i t-att-class="firstAction.icon"/></button>
</t>
<t t-if="actions.length === 3">
<t t-set="secondAction" t-value="orderedActions[1]"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 opacity-75 opacity-100-hover" t-att-title="secondAction.name" t-on-click.stop="secondAction.onSelect"><i t-att-class="secondAction.icon"/></button>
<t t-if="threadActions.actions.length === 3">
<t t-set="secondAction" t-value="threadActions.actions[1]"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 opacity-75 opacity-100-hover" t-att-title="secondAction.name" t-on-click.stop="() => secondAction.onSelect()"><i t-att-class="secondAction.icon"/></button>
</t>
<t t-elif="actions.length > 3">
<t t-elif="threadActions.actions.length > 3">
<Dropdown position="'bottom-end'" onStateChanged="state => this.onMoreActionsStateChanged(state)" togglerClass="'o-mail-ChatWindow-command d-flex btn p-0 p-2'" menuClass="'d-flex flex-column py-0'" class="'d-flex'" title="moreMenuText">
<t t-set-slot="toggler">
<i class="fa fa-fw fa-ellipsis-h" tabindex="1"/>
</t>
<t t-set-slot="default">
<DropdownItem t-foreach="orderedActions.slice(1, -1)" t-as="action" t-key="action.id" class="'o-mail-ChatWindow-command btn rounded-0 d-flex align-items-center px-2 py-2 m-0 opacity-75 opacity-100-hover'" title="action.name" onSelected="action.onSelect"><i t-att-class="action.icon"/><span class="mx-2" t-out="action.name"/></DropdownItem>
<DropdownItem t-foreach="threadActions.actions.slice(1, -1)" t-as="action" t-key="action.id" class="'o-mail-ChatWindow-command btn rounded-0 d-flex align-items-center px-2 py-2 m-0 opacity-75 opacity-100-hover'" title="action.name" onSelected="() => action.onSelect()"><i t-att-class="action.icon"/><span class="mx-2" t-out="action.name"/></DropdownItem>
</t>
</Dropdown>
</t>
<t t-if="actions.length > 0">
<t t-set="lastAction" t-value="orderedActions.at(-1)"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 me-1 opacity-75 opacity-100-hover" t-att-title="lastAction.name" t-on-click.stop="lastAction.onSelect"><i t-att-class="lastAction.icon"/></button>
<t t-if="threadActions.actions.length > 0">
<t t-set="lastAction" t-value="threadActions.actions.at(-1)"/>
<button class="o-mail-ChatWindow-command btn d-flex p-2 me-1 opacity-75 opacity-100-hover" t-att-title="lastAction.name" t-on-click.stop="() => lastAction.onSelect()"><i t-att-class="lastAction.icon"/></button>
</t>
</div>
<div t-if="!props.chatWindow.folded" class="o-mail-ChatWindow-content bg-view d-flex flex-column h-100 overflow-auto position-relative" t-ref="content">
<button t-if="state.activeMode" class="o-mail-ChatWindow-closePanel btn position-absolute top-0 end-0 btn opacity-75 opacity-100-hover" title="Close panel" t-on-click.stop="() => state.activeMode = ''"><i class="fa fa-close"/></button>
<button t-if="threadActions.activeAction" class="o-mail-ChatWindow-closePanel btn position-absolute top-0 end-0 btn opacity-75 opacity-100-hover" title="Close panel" t-on-click.stop="() => threadActions.activeAction.close()"><i class="fa fa-close"/></button>
<t t-if="thread" name="thread content">
<t t-if="state.activeMode === MODES.NONE">
<t t-if="threadActions.activeAction?.componentCondition" t-component="threadActions.activeAction.component" thread="thread" t-props="threadActions.activeAction.componentProps"/>
<t t-else="">
<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"/>
@@ -0,0 +1,111 @@
/* @odoo-module */
import { useComponent, useState } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
export const threadActionsRegistry = registry.category("mail.thread/actions");
threadActionsRegistry.add("close", {
condition(component) {
return component.props.chatWindow;
},
icon: "fa fa-fw fa-close",
name: _t("Close Chat Window"),
open(component) {
component.close();
},
sequence: 100,
});
function transformAction(component, id, action) {
return {
/** Closes this action. */
close() {
if (this.toggle) {
component.threadActions.activeAction = null;
}
action.close?.(component, this);
},
/** Optional component that should be displayed in the view when this action is active. */
component: action.component,
/** Condition to display the component of this action. */
get componentCondition() {
return this.isActive && this.component && this.condition && !this.popover;
},
/** Props to pass to the component of this action. */
componentProps: action.componentProps,
/** Condition to display this action. */
get condition() {
return action.condition(component);
},
/** Condition to disable the button of this action (but still display it). */
get disabledCondition() {
return action.disabledCondition?.(component);
},
/** Icon for the button this action. */
icon: action.icon,
/** Large icon for the button this action. */
iconLarge: action.iconLarge ?? action.icon,
/** Unique id of this action. */
id,
/** States whether this action is currently active. */
get isActive() {
return id === component.threadActions.activeAction?.id;
},
/** Name of this action, displayed to the user. */
get name() {
return this.isActive && action.nameActive ? action.nameActive : action.name;
},
/** Action to execute when this action is selected (on or off). */
onSelect() {
if (this.toggle && this.isActive) {
this.close();
} else {
this.open();
}
},
/** Opens this action. */
open() {
if (this.toggle) {
component.threadActions.activeAction = this;
}
action.open?.(component, this);
},
/** Determines whether this is a popover linked to this action. */
popover: null,
/** Determines the order of this action (smaller first). */
get sequence() {
return typeof action.sequence === "function"
? action.sequence(component)
: action.sequence;
},
/** Component setup to execute when this action is registered. */
setup: action.setup,
/** Text for the button of this action */
text: action.text,
/** Determines whether this action is a one time effect or can be toggled (on or off). */
toggle: action.toggle,
};
}
export function useThreadActions() {
const component = useComponent();
const transformedActions = threadActionsRegistry
.getEntries()
.map(([id, action]) => transformAction(component, id, action));
for (const action of transformedActions) {
if (action.setup) {
action.setup(action);
}
}
const state = useState({
get actions() {
return transformedActions
.filter((action) => action.condition)
.sort((a1, a2) => a1.sequence - a2.sequence);
},
activeAction: null,
});
return state;
}
@@ -12,7 +12,7 @@ $o-mail-LinkPreviewCard-height: 80px !default;
$o-mail-Message-sidebarWidth: 48px !default;
$o-mail-NavigableList-zIndex: 11;
$o-mail-Chatter-minWidth: 530px !default;
$o-mail-Discuss-inspector: 250px !default;
$o-mail-Discuss-inspector: 300px !default;
$o-mail-Discuss-headerHeight: 48px !default;
@mixin o-FileViewer-arrow {
@@ -0,0 +1,42 @@
/* @odoo-module */
import { threadActionsRegistry } from "@mail/core/thread_actions";
import { CallSettings } from "@mail/discuss/core/call_settings";
import { _t } from "@web/core/l10n/translation";
threadActionsRegistry
.add("call", {
condition(component) {
return (
component.thread?.allowCalls &&
component.thread !== component.rtc.state.channel &&
!component.props.chatWindow?.hidden
);
},
icon: "fa fa-fw fa-phone",
iconLarge: "fa fa-fw fa-lg fa-phone",
name: _t("Start a Call"),
open(component) {
component.rtc.toggleCall(component.thread);
},
sequence: 10,
})
.add("settings", {
component: CallSettings,
condition(component) {
return (
component.thread?.allowCalls &&
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
icon: "fa fa-fw fa-gear",
iconLarge: "fa fa-fw fa-lg fa-gear",
name: _t("Show Call Settings"),
nameActive: _t("Hide Call Settings"),
sequence(component) {
return component.props.chatWindow && component.thread === component.rtc.state.channel
? 6
: 60;
},
toggle: true,
});
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="discuss.CallSettings" owl="1">
<div class="o-discuss-CallSettings py-2 user-select-none bg-view" t-attf-class="{{ props.className }}" t-ref="root">
<div class="o-discuss-CallSettings flex-grow-1 py-2 user-select-none bg-view" t-attf-class="{{ props.className }}" t-ref="root">
<div class="d-flex flex-column px-3 overflow-auto">
<div class="py-2 fw-bolder text-700 text-truncate text-uppercase">Voice Settings</div>
<div class="mb-3 d-flex align-items-center flex-wrap">
@@ -10,7 +10,7 @@ import { useService } from "@web/core/utils/hooks";
export class ChannelInvitation extends Component {
static components = { ImStatus };
static defaultProps = { hasSizeConstraints: false };
static props = ["hasSizeConstraints?", "thread", "close?", "chatState?"];
static props = ["hasSizeConstraints?", "thread", "close", "className?"];
static template = "discuss.ChannelInvitation";
setup() {
@@ -114,11 +114,7 @@ export class ChannelInvitation extends Component {
}
);
}
if (this.ui.isSmall) {
this.props.chatState.activeMode = "";
} else {
this.props.close();
}
this.props.close();
}
get invitationButtonText() {
@@ -1,3 +1,7 @@
.o-discuss-ChannelInvitation {
min-height: 0;
}
.o-discuss-ChannelInvitation-has-size-constraints {
width: Min(95vw, 400px);
max-height: Min(calc(100vh - 140px), 530px);
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="discuss.ChannelInvitation" owl="1">
<div class="o-discuss-ChannelInvitation bg-view d-flex flex-column" t-att-class="{ 'o-discuss-ChannelInvitation-has-size-constraints': props.hasSizeConstraints }">
<div class="o-discuss-ChannelInvitation bg-view d-flex flex-column" t-att-class="{ 'o-discuss-ChannelInvitation-has-size-constraints': props.hasSizeConstraints }" t-attf-class="{{ props.className }}">
<t t-if="store.user">
<h3 class="mx-3 mt-3 mb-2">Invite people</h3>
<div class="mx-3 my-2">
@@ -7,7 +7,7 @@ import { useService } from "@web/core/utils/hooks";
export class ChannelMemberList extends Component {
static components = { ImStatus };
static props = ["thread", "className"];
static props = ["thread", "className?"];
static template = "discuss.ChannelMemberList";
setup() {
@@ -1,45 +0,0 @@
/* @odoo-module */
import { CallSettings } from "@mail/discuss/core/call_settings";
import { ChannelInvitation } from "@mail/discuss/core/channel_invitation";
import { ChannelMemberList } from "@mail/discuss/core/channel_member_list";
import { Discuss } from "@mail/discuss_app/discuss";
import { useRef } from "@odoo/owl";
import { usePopover } from "@web/core/popover/popover_hook";
import { patch } from "@web/core/utils/patch";
patch(Discuss, "discuss", {
components: { ...Discuss.components, CallSettings, ChannelMemberList },
});
patch(Discuss.prototype, "discuss", {
setup() {
this._super();
this.addUsersRef = useRef("addUsers");
this.popover = usePopover(ChannelInvitation, {
onClose: () => (this.state.isAddingUsers = false),
});
this.state.isAddingUsers = false;
},
toggleInviteForm() {
if (this.popover.isOpen) {
this.popover.close();
} else {
this.state.isAddingUsers = true;
this.popover.open(this.addUsersRef.el, {
hasSizeConstraints: true,
thread: this.thread,
});
}
},
toggleMemberList() {
this.state.activeMode =
this.state.activeMode === this.MODES.MEMBER_LIST
? this.MODES.NONE
: this.MODES.MEMBER_LIST;
},
toggleSettings() {
this.state.activeMode =
this.state.activeMode === this.MODES.SETTINGS ? this.MODES.NONE : this.MODES.SETTINGS;
},
});
@@ -1,7 +0,0 @@
.o-mail-Discuss-callSettings {
width: 300px;
}
.o-mail-Discuss-channelMemberList {
width: $o-mail-Discuss-inspector;
}
@@ -1,20 +0,0 @@
<?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="after">
<button t-if="thread.model === 'discuss.channel'" class="btn rounded px-2 shadow-none" t-att-class="{ 'o-isActive': state.isAddingUsers }" title="Add Users" t-on-click.stop="toggleInviteForm" t-ref="addUsers">
<i class="fa fa-lg fa-user-plus text-700"/>
</button>
<button t-if="thread.hasMemberList" class="btn rounded px-2 shadow-none" t-att-class="{ 'o-isActive': state.activeMode === MODES.MEMBER_LIST }" t-att-title="state.activeMode === MODES.MEMBER_LIST ? 'Hide Member List' : 'Show Member List'" t-on-click.stop="toggleMemberList">
<i class="fa fa-lg fa-users text-700"/>
</button>
<button t-if="thread.allowCalls" class="btn rounded px-2 shadow-none" t-att-class="{ 'o-isActive': state.activeMode === MODES.SETTINGS }" t-att-title="state.activeMode === MODES.SETTINGS ? 'Hide Call Settings' : 'Show Call Settings'" t-on-click="toggleSettings">
<i class="fa fa-lg fa-gear text-700"/>
</button>
</xpath>
<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>
</t>
</templates>
@@ -0,0 +1,60 @@
/* @odoo-module */
import { threadActionsRegistry } from "@mail/core/thread_actions";
import { ChannelInvitation } from "@mail/discuss/core/channel_invitation";
import { ChannelMemberList } from "@mail/discuss/core/channel_member_list";
import { useComponent } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { usePopover } from "@web/core/popover/popover_hook";
threadActionsRegistry
.add("add-users", {
close(component, action) {
action.popover?.close();
},
component: ChannelInvitation,
componentProps(action) {
return { close: () => action.close() };
},
condition(component) {
return (
component.thread?.model === "discuss.channel" &&
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
icon: "fa fa-fw fa-user-plus",
iconLarge: "fa fa-fw fa-lg fa-user-plus",
name: _t("Add Users"),
nameActive: _t("Stop Adding Users"),
open(component, action) {
action.popover?.open(component.root.el.querySelector(`[name="${action.id}"]`), {
hasSizeConstraints: true,
thread: component.thread,
});
},
sequence: 30,
setup(action) {
const component = useComponent();
if (!component.props.chatWindow) {
action.popover = usePopover(ChannelInvitation, {
onClose: () => action.close(),
});
}
},
toggle: true,
})
.add("member-list", {
component: ChannelMemberList,
condition(component) {
return (
component.thread?.hasMemberList &&
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
icon: "fa fa-fw fa-users",
iconLarge: "fa fa-fw fa-lg fa-users",
name: _t("Show Member List"),
nameActive: _t("Hide Member List"),
sequence: 40,
toggle: true,
});
@@ -1,32 +0,0 @@
/* @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;
},
});
@@ -1,13 +0,0 @@
<?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>
@@ -11,7 +11,7 @@ export class PinnedMessagesPanel extends Component {
static components = {
Message,
};
static props = ["thread"];
static props = ["thread", "className?"];
static template = "discuss.PinnedMessagesPanel";
setup() {
@@ -3,8 +3,7 @@
<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" t-att-class="{
'flex-grow-1': env.inChatWindow,
'border-start': !env.inChatWindow,
}">
}" t-attf-class="{{ props.className }}">
<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>
@@ -0,0 +1,34 @@
/* @odoo-module */
import { threadActionsRegistry } from "@mail/core/thread_actions";
import { PinnedMessagesPanel } from "@mail/discuss/message_pin/pinned_messages_panel";
import { useChildSubEnv } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
threadActionsRegistry.add("pinned-messages", {
component: PinnedMessagesPanel,
condition(component) {
return (
component.thread?.model === "discuss.channel" &&
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
icon: "fa fa-fw fa-thumb-tack",
iconLarge: "fa fa-fw fa-lg fa-thumb-tack",
name: _t("Pinned Messages"),
nameActive: _t("Hide Pinned Messages"),
sequence: 20,
setup(action) {
useChildSubEnv({
pinMenu: {
open: () => action.open(),
close: () => {
if (action.isActive) {
action.close();
}
},
},
});
},
toggle: true,
});
@@ -1,15 +1,9 @@
/* @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
@@ -1,46 +0,0 @@
/* @odoo-module */
import { PinnedMessagesPanel } from "@mail/discuss/message_pin/pinned_messages_panel";
import { ChatWindow, MODES } from "@mail/chat_window/chat_window";
import { useChildSubEnv } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
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;
},
get actions() {
const acts = this._super();
if (this.thread?.model === "discuss.channel" && this.props.chatWindow.isOpen) {
acts.push({
id: "pinned",
name: _t("Pinned Messages"),
icon: "fa fa-fw fa-thumb-tack",
onSelect: () => this.togglePinMenu(),
sequence: 20,
});
}
return acts;
},
});
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ChatWindow" t-inherit-mode="extension">
<xpath expr="//t[@name='thread content']" position="inside">
<PinnedMessagesPanel t-if="state.activeMode === MODES.PINNED_MESSAGES" thread="thread"/>
</xpath>
</t>
</templates>
@@ -1,64 +0,0 @@
/* @odoo-module */
import { CallSettings } from "@mail/discuss/core/call_settings";
import { ChannelInvitation } from "@mail/discuss/core/channel_invitation";
import { ChannelMemberList } from "@mail/discuss/core/channel_member_list";
import { ChatWindow } from "@mail/chat_window/chat_window";
import { patch } from "@web/core/utils/patch";
import { _t } from "@web/core/l10n/translation";
patch(ChatWindow, "discuss", {
components: { ...ChatWindow.components, CallSettings, ChannelInvitation, ChannelMemberList },
});
patch(ChatWindow.prototype, "discuss", {
toggleAddUsers() {
this.state.activeMode = this.state.activeMode === "add-users" ? "" : "add-users";
},
toggleMemberList() {
this.state.activeMode = this.state.activeMode === "member-list" ? "" : "member-list";
},
toggleSettings() {
this.state.activeMode = this.state.activeMode === "in-settings" ? "" : "in-settings";
},
get actions() {
const acts = this._super();
if (this.props.chatWindow.isOpen && this.thread?.model === "discuss.channel") {
acts.push({
id: "invite",
name:
this.state.activeMode === "add-users"
? _t("Stop Adding Users")
: _t("Add Users"),
icon: "fa fa-fw fa-user-plus",
onSelect: () => this.toggleAddUsers(),
sequence: 30,
});
}
if (this.thread?.hasMemberList && this.props.chatWindow.isOpen) {
acts.push({
id: "member",
name:
this.state.activeMode === "member-list"
? _t("Hide Member List")
: _t("Show Member List"),
icon: "fa fa-fw fa-users",
onSelect: () => this.toggleMemberList(),
sequence: 40,
});
}
if (this.thread?.allowCalls && this.props.chatWindow.isOpen) {
acts.push({
id: "settings",
name:
this.state.activeMode === "in-settings"
? _t("Hide Call Settings")
: _t("Show Call Settings"),
icon: "fa fa-fw fa-gear",
onSelect: () => this.toggleSettings(),
sequence: this.thread === this.rtc.state.channel ? 6 : 60,
});
}
return acts;
},
});
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ChatWindow" t-inherit-mode="extension">
<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 w-100 h-100'"/>
<ChannelInvitation t-if="state.activeMode === 'add-users'" thread="thread" chatState="state"/>
</xpath>
</t>
</templates>
@@ -0,0 +1,29 @@
/* @odoo-module */
import { threadActionsRegistry } from "@mail/core/thread_actions";
import { _t } from "@web/core/l10n/translation";
threadActionsRegistry.add("expand-discuss", {
condition(component) {
return (
component.thread &&
component.props.chatWindow?.isOpen &&
component.thread.model === "discuss.channel" &&
!component.ui.isSmall
);
},
icon: "fa fa-fw fa-expand",
name: _t("Open in Discuss"),
open(component) {
component.threadService.setDiscussThread(component.thread);
component.actionService.doAction(
{
type: "ir.actions.client",
tag: "mail.action_discuss",
name: _t("Discuss"),
},
{ clearBreadcrumbs: true }
);
},
sequence: 51,
});
+3 -12
View File
@@ -2,6 +2,7 @@
import { ImStatus } from "./im_status";
import { AutoresizeInput } from "./autoresize_input";
import { useThreadActions } from "@mail/core/thread_actions";
import { Thread } from "../core_ui/thread";
import { ThreadIcon } from "./thread_icon";
import { useMessaging, useStore } from "../core/messaging_hook";
@@ -24,12 +25,6 @@ import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { url } from "@web/core/utils/urls";
export const MODES = {
MEMBER_LIST: "member-list",
SETTINGS: "settings",
NONE: "",
};
export class Discuss extends Component {
static components = {
AutoresizeInput,
@@ -44,7 +39,6 @@ export class Discuss extends Component {
static template = "mail.Discuss";
setup() {
this.MODES = MODES;
this.messaging = useMessaging();
this.store = useStore();
this.threadService = useState(useService("mail.thread"));
@@ -55,7 +49,7 @@ export class Discuss extends Component {
this.messageEdition = useMessageEdition();
this.messageToReplyTo = useMessageToReplyTo();
this.contentRef = useRef("content");
this.state = useState({ activeMode: MODES.NONE });
this.root = useRef("root");
this.orm = useService("orm");
this.effect = useService("effect");
this.ui = useState(useService("ui"));
@@ -65,6 +59,7 @@ export class Discuss extends Component {
messageHighlight: this.messageHighlight,
});
this.notification = useService("notification");
this.threadActions = useThreadActions();
useEffect(
() => {
if (
@@ -87,10 +82,6 @@ export class Discuss extends Component {
onWillUnmount(() => (this.store.discuss.isActive = false));
}
markAllAsRead() {
this.orm.silent.call("mail.message", "mark_all_as_read");
}
get thread() {
return this.store.threads[this.store.discuss.threadLocalId];
}
@@ -4,6 +4,10 @@
}
}
.o-mail-Discuss-inspector {
width: $o-mail-Discuss-inspector;
}
.o-mail-Discuss-selfAvatar {
height: $o-mail-Avatar-sizeSmall;
width: $o-mail-Avatar-sizeSmall;
@@ -40,11 +40,11 @@
</t>
</div>
<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.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>
<t t-foreach="threadActions.actions" t-as="action" t-key="action.id">
<button class="btn px-2" t-attf-class="{{ action.isActive ? 'o-isActive' : '' }}" t-att-disabled="action.disabledCondition" t-att-title="action.name" t-att-name="action.id" t-on-click="() => action.onSelect()">
<i t-if="action.iconLarge" t-att-class="action.iconLarge"/> <span t-if="action.text" t-esc="action.text"/>
</button>
</t>
<div t-if="store.inPublicPage and !ui.isSmall" class="d-flex align-items-center">
<img class="o-mail-Discuss-selfAvatar mx-1 rounded-circle o_object_fit_cover flex-shrink-0" alt="Avatar" t-att-src="threadService.avatarUrl(store.self, thread)"/>
<div class="lead fw-bold flex-shrink-1 text-dark">
@@ -66,6 +66,7 @@
<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>
<t t-if="threadActions.activeAction?.componentCondition" t-component="threadActions.activeAction.component" thread="thread" className="'o-mail-Discuss-inspector border-start'" t-props="threadActions.activeAction.componentProps"/>
</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">
@@ -2,7 +2,7 @@ $o-mail-Avatar-size: 42px !default;
$o-mail-ChatWindow-width: 360px !default; // same value as CHAT_WINDOW_WIDTH
$o-mail-Chatter-mobile-gap: 2% !default;
$o-mail-sidebar-icon-opacity: 0.7 !default;
$o-mail-Discuss-inspector: 250px !default;
$o-mail-Discuss-inspector: 300px !default;
$o-mail-Avatar-sizeSmall: 24px !default;
$o-mail-partner-status-icon-size: 1.2em !default;
// Needed because $border-radius variations are all set to 0 in enterprise.
@@ -3,7 +3,6 @@
import { ChatWindow } from "@mail/chat_window/chat_window";
import { ChannelSelector } from "@mail/discuss_app/channel_selector";
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
@@ -14,43 +13,4 @@ patch(ChatWindow.prototype, "mail/chat_window", {
this._super(...arguments);
this.actionService = useService("action");
},
expand() {
if (this.thread.type === "chatter") {
this.actionService.doAction({
type: "ir.actions.act_window",
res_id: this.thread.id,
res_model: this.thread.model,
views: [[false, "form"]],
});
this.chatWindowService.close(this.props.chatWindow);
return;
}
this.threadService.setDiscussThread(this.thread);
this.actionService.doAction(
{
type: "ir.actions.client",
tag: "mail.action_discuss",
name: _t("Discuss"),
},
{ clearBreadcrumbs: true }
);
},
get actions() {
const acts = this._super();
if (this.thread && this.props.chatWindow.isOpen) {
acts.splice(acts.length - 1, 0, {
id: "expand",
name:
this.thread.model === "discuss.channel"
? _t("Open in Discuss")
: _t("Open Form View"),
icon: "fa fa-fw fa-expand",
onSelect: () => this.expand(),
sequence: 50,
});
}
return acts;
},
});
@@ -0,0 +1,49 @@
/* @odoo-module */
import { threadActionsRegistry } from "@mail/core/thread_actions";
import { _t } from "@web/core/l10n/translation";
threadActionsRegistry
.add("mark-all-read", {
condition(component) {
return component.thread?.id === "inbox";
},
disabledCondition(component) {
return component.thread.isEmpty;
},
open(component) {
component.orm.silent.call("mail.message", "mark_all_as_read");
},
sequence: 1,
text: _t("Mark all read"),
})
.add("unstar-all", {
condition(component) {
return component.thread?.id === "starred";
},
disabledCondition(component) {
return component.thread.isEmpty;
},
open(component) {
component.messageService.unstarAll();
},
sequence: 2,
text: _t("Unstar all"),
})
.add("expand-form", {
condition(component) {
return component.thread?.type === "chatter" && component.props.chatWindow?.isOpen;
},
icon: "fa fa-fw fa-expand",
name: _t("Open Form View"),
open(component) {
component.actionService.doAction({
type: "ir.actions.act_window",
res_id: component.thread.id,
res_model: component.thread.model,
views: [[false, "form"]],
});
component.chatWindowService.close(component.props.chatWindow);
},
sequence: 50,
});
@@ -17,6 +17,7 @@ QUnit.test("Add member to channel", async (assert) => {
await click("[title='Add Users']");
await click(".o-discuss-ChannelInvitation-selectable:contains(Harry)");
await click("[title='Invite to Channel']");
await click("[title='Show Member List']");
assert.containsOnce($, ".o-discuss-ChannelMember:contains(Harry)");
});
@@ -15,7 +15,7 @@ QUnit.test("Pin message", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await click(".o-mail-Discuss-header button[title='Pinned messages']");
await click(".o-mail-Discuss-header button[title='Pinned Messages']");
assert.containsOnce(
$,
".o-discuss-PinnedMessagesPanel:contains(This channel doesn't have any pinned messages.)"
@@ -37,7 +37,7 @@ QUnit.test("Unpin message", async (assert) => {
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await click(".o-mail-Discuss-header button[title='Pinned messages']");
await click(".o-mail-Discuss-header button[title='Pinned Messages']");
assert.containsOnce($, ".o-discuss-PinnedMessagesPanel .o-mail-Message");
await click(".o-mail-Message [title='Expand']");
await click(".dropdown-item:contains(Unpin)");
@@ -57,7 +57,7 @@ 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']");
await click(".o-mail-Discuss-header button[title='Pinned Messages']");
assert.containsOnce($, ".o-discuss-PinnedMessagesPanel .o-mail-Message");
await click(".o-mail-Message [title='Expand']");
await click(".dropdown-item:contains(Delete)");
@@ -108,7 +108,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-Discuss-header button[title='Pinned Messages']");
await click(".o-discuss-PinnedMessagesPanel button:contains(Jump)");
await nextTick();
assert.isVisible($(".o-mail-Message:contains(Hello world!)"));