[IMP] mail: open and join call on click on the start a meeting button

Automatically create and open a new channel, then start a call, on click
on the "Start a meeting" button.

Part of task-2651831.

closes odoo/odoo#77946

X-original-commit: cedc8e934bf8c97f94f455389315f9f4c40137c7
Signed-off-by: Samuel Degueldre <sdegueldre@users.noreply.github.com>
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Louis Wicket (wil)
2021-10-06 10:27:12 +00:00
committed by Samuel Degueldre
parent d5db1b9500
commit 021bc1bfd8
19 changed files with 372 additions and 196 deletions
@@ -3,66 +3,68 @@
<t t-name="mail.ChannelInvitationForm" owl="1">
<div class="o_ChannelInvitationForm d-flex flex-column" t-ref="channelInvitationForm">
<h3 class="mx-3 mt-3 mb-2">Invite people</h3>
<t t-if="!messaging.isCurrentUserGuest">
<div class="mx-3 my-2">
<input class="o_ChannelInvitationForm_searchInput" type="text" t-att-value="channelInvitationForm.searchTerm" placeholder="Type the name of a person" t-on-input="channelInvitationForm.onInputSearch" t-ref="searchInput"/>
</div>
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
<t t-foreach="channelInvitationForm.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.localId">
<div class="o_ChannelInvitationForm_selectablePartner d-flex align-items-center px-3 py-1" t-on-click="channelInvitationForm.onClickSelectablePartner.bind(channelInvitationForm, selectablePartner)" t-att-data-partner-id="selectablePartner.id">
<div class="o_ChannelInvitationForm_selectablePartnerAvatarContainer flex-shrink-0 position-relative">
<img class="o_ChannelInvitationForm_selectablePartnerAvatar w-100 h-100 rounded-circle" t-att-src="selectablePartner.avatarUrl" alt="Avatar"/>
<t t-if="selectablePartner.im_status and selectablePartner.im_status !== 'im_partner'">
<PartnerImStatusIcon
class="o_ChannelInvitationForm_selectablePartnerImStatusIcon d-flex align-items-center justify-content-center text-white"
t-att-class="{
'o_ChannelInvitationForm_selectablePartnerImStatusIcon-mobile': messaging.device.isMobile,
}"
partnerLocalId="selectablePartner.localId"
/>
<t t-if="channelInvitationForm">
<h3 class="mx-3 mt-3 mb-2">Invite people</h3>
<t t-if="!messaging.isCurrentUserGuest">
<div class="mx-3 my-2">
<input class="o_ChannelInvitationForm_searchInput" type="text" t-att-value="channelInvitationForm.searchTerm" placeholder="Type the name of a person" t-on-input="channelInvitationForm.onInputSearch" t-ref="searchInput"/>
</div>
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
<t t-foreach="channelInvitationForm.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.localId">
<div class="o_ChannelInvitationForm_selectablePartner d-flex align-items-center px-3 py-1" t-on-click="channelInvitationForm.onClickSelectablePartner.bind(channelInvitationForm, selectablePartner)" t-att-data-partner-id="selectablePartner.id">
<div class="o_ChannelInvitationForm_selectablePartnerAvatarContainer flex-shrink-0 position-relative">
<img class="o_ChannelInvitationForm_selectablePartnerAvatar w-100 h-100 rounded-circle" t-att-src="selectablePartner.avatarUrl" alt="Avatar"/>
<t t-if="selectablePartner.im_status and selectablePartner.im_status !== 'im_partner'">
<PartnerImStatusIcon
class="o_ChannelInvitationForm_selectablePartnerImStatusIcon d-flex align-items-center justify-content-center text-white"
t-att-class="{
'o_ChannelInvitationForm_selectablePartnerImStatusIcon-mobile': messaging.device.isMobile,
}"
partnerLocalId="selectablePartner.localId"
/>
</t>
</div>
<div class="o_ChannelInvitationForm_selectablePartnerAvatarNameSeparator"/>
<span class="o_ChannelInvitationForm_selectablePartnerName flex-grow-1 text-truncate">
<t t-esc="selectablePartner.nameOrDisplayName"/>
</span>
<div class="o_ChannelInvitationForm_selectablePartnerNameSelectionSeparator flex-grow-1 flex-shrink-0"/>
<input class="o_ChannelInvitationForm_selectablePartnerCheckbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="channelInvitationForm.onInputPartnerCheckbox.bind(channelInvitationForm, selectablePartner)" t-ref="selection-status"/>
</div>
</t>
<t t-if="channelInvitationForm.selectablePartners.length === 0">
<div class="mx-3">No user found that is not already a member of this channel.</div>
</t>
<t t-if="channelInvitationForm.searchResultCount > channelInvitationForm.selectablePartners.length">
<div class="mx-3">
Showing <t t-esc="channelInvitationForm.selectablePartners.length"/> results out of <t t-esc="channelInvitationForm.searchResultCount"/>. Narrow your search to see more choices.
</div>
</t>
</div>
<t t-if="channelInvitationForm.selectedPartners.length > 0">
<div class="mx-3 mt-3">
<h4>Selected users:</h4>
<div class="o_ChannelInvitationForm_selectedPartners overflow-auto">
<t t-foreach="channelInvitationForm.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.localId">
<button class="btn btn-secondary" t-on-click="channelInvitationForm.onClickSelectedPartner.bind(channelInvitationForm, selectedPartner)">
<t t-esc="selectedPartner.nameOrDisplayName"/> <i class="fa fa-times"/>
</button>
</t>
</div>
<div class="o_ChannelInvitationForm_selectablePartnerAvatarNameSeparator"/>
<span class="o_ChannelInvitationForm_selectablePartnerName flex-grow-1 text-truncate">
<t t-esc="selectablePartner.nameOrDisplayName"/>
</span>
<div class="o_ChannelInvitationForm_selectablePartnerNameSelectionSeparator flex-grow-1 flex-shrink-0"/>
<input class="o_ChannelInvitationForm_selectablePartnerCheckbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="channelInvitationForm.onInputPartnerCheckbox.bind(channelInvitationForm, selectablePartner)" t-ref="selection-status"/>
</div>
</t>
<t t-if="channelInvitationForm.selectablePartners.length === 0">
<div class="mx-3">No user found that is not already a member of this channel.</div>
</t>
<t t-if="channelInvitationForm.searchResultCount > channelInvitationForm.selectablePartners.length">
<div class="mx-3">
Showing <t t-esc="channelInvitationForm.selectablePartners.length"/> results out of <t t-esc="channelInvitationForm.searchResultCount"/>. Narrow your search to see more choices.
</div>
</t>
</div>
<t t-if="channelInvitationForm.selectedPartners.length > 0">
<div class="mx-3 mt-3">
<h4>Selected users:</h4>
<div class="o_ChannelInvitationForm_selectedPartners overflow-auto">
<t t-foreach="channelInvitationForm.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.localId">
<button class="btn btn-secondary" t-on-click="channelInvitationForm.onClickSelectedPartner.bind(channelInvitationForm, selectedPartner)">
<t t-esc="selectedPartner.nameOrDisplayName"/> <i class="fa fa-times"/>
</button>
</t>
</div>
<div class="mx-3 mt-2 mb-3">
<button class="o_ChannelInvitationForm_inviteButton btn btn-primary w-100" t-att-disabled="channelInvitationForm.selectedPartners.length === 0" t-on-click="channelInvitationForm.onClickInvite">
<t t-esc="channelInvitationForm.inviteButtonText"/>
</button>
</div>
</t>
<t t-if="channelInvitationForm.thread and channelInvitationForm.thread.invitationLink">
<h4 class="mx-3 mt-3 mb-2">Invitation Link</h4>
<div class="mx-3 my-2">
<input type="text" t-att-value="channelInvitationForm.thread.invitationLink"/>
</div>
</t>
<div class="mx-3 mt-2 mb-3">
<button class="o_ChannelInvitationForm_inviteButton btn btn-primary w-100" t-att-disabled="channelInvitationForm.selectedPartners.length === 0" t-on-click="channelInvitationForm.onClickInvite">
<t t-esc="channelInvitationForm.inviteButtonText"/>
</button>
</div>
</t>
<t t-if="channelInvitationForm.thread and channelInvitationForm.thread.invitationLink">
<h4 class="mx-3 mt-3 mb-2">Invitation Link</h4>
<div class="mx-3 my-2">
<input type="text" t-att-value="channelInvitationForm.thread.invitationLink"/>
</div>
</t>
</div>
</t>
@@ -13,7 +13,7 @@
</t>
<t t-else="">
<t t-if="!messaging.device.isMobile">
<DiscussSidebar class="o_Discuss_sidebar bg-light border-right"/>
<DiscussSidebar class="o_Discuss_sidebar bg-light border-right" localId="discuss.localId"/>
</t>
<t t-if="messaging.device.isMobile" t-call="mail.Discuss.content"/>
<t t-else="">
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
import { useUpdate } from '@mail/component_hooks/use_update/use_update';
const { Component } = owl;
@@ -14,6 +15,7 @@ export class DiscussSidebar extends Component {
constructor(...args) {
super(...args);
useUpdate({ func: () => this._update() });
useRefToModel({ fieldName: 'startAMeetingButtonRef', modelName: 'mail.discuss', propNameAsRecordLocalId: 'localId', refName: 'startAMeetingButton' });
/**
* Reference of the quick search input. Useful to filter channels and
* chats based on this input content.
@@ -29,7 +31,7 @@ export class DiscussSidebar extends Component {
* @returns {mail.discuss}
*/
get discuss() {
return this.messaging && this.messaging.discuss;
return this.messaging && this.messaging.models['mail.discuss'].get(this.props.localId);
}
//--------------------------------------------------------------------------
@@ -64,7 +66,9 @@ export class DiscussSidebar extends Component {
}
Object.assign(DiscussSidebar, {
props: {},
props: {
localId: String
},
template: 'mail.DiscussSidebar',
});
@@ -3,16 +3,11 @@
<t t-name="mail.DiscussSidebar" owl="1">
<div name="root" class="o_DiscussSidebar">
<Popover position="'right'">
<div class="d-flex justify-content-center">
<button class="btn btn-primary rounded" title="Start a meeting" t-on-click="discuss.onClickStartAMeetingButton">
Start a meeting
</button>
</div>
<t t-set="opened">
<ChannelInvitationForm t-if="discuss.channelInvitationForm" localId="discuss.channelInvitationForm.localId"/>
</t>
</Popover>
<div class="d-flex justify-content-center">
<button class="o_DiscussSidebar_startAMeetingButton btn btn-primary rounded" title="Start a meeting" t-on-click="discuss.onClickStartAMeetingButton" t-ref="startAMeetingButton">
Start a meeting
</button>
</div>
<hr class="o_DiscussSidebar_separator"/>
<div class="o_DiscussSidebar_category o_DiscussSidebar_categoryMailbox">
<DiscussSidebarMailbox threadLocalId="messaging.inbox.localId"/>
@@ -0,0 +1,44 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { usePosition } from '@web/core/position/position_hook';
const { Component } = owl;
export class PopoverView extends Component {
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'mail.popover_view', propNameAsRecordLocalId: 'popoverViewLocalId' });
usePosition(
() => this.popoverView && this.popoverView.anchorRef && this.popoverView.anchorRef.el,
{
margin: 16,
position: this.popoverView.position,
}
);
}
//--------------------------------------------------------------------------
// Getters / Setters
//--------------------------------------------------------------------------
/**
* @returns {mail.popover_view|undefined}
*/
get popoverView() {
return this.messaging && this.messaging.models['mail.popover_view'].get(this.props.popoverViewLocalId);
}
}
Object.assign(PopoverView, {
props: {
popoverViewLocalId: String,
},
template: 'mail.PopoverView',
});
registerMessagingComponent(PopoverView);
@@ -0,0 +1,18 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_PopoverView {
z-index: $zindex-modal + 1;
border: $border-width solid;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_PopoverView {
border-color: gray('300');
background-color: $white;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.PopoverView" owl="1">
<div class="o_PopoverView">
<t t-if="popoverView">
<ChannelInvitationForm localId="popoverView.channelInvitationForm.localId"/>
</t>
</div>
</t>
</templates>
@@ -14,6 +14,7 @@ export class ThreadViewTopbar extends Component {
setup() {
super.setup();
useRefToModel({ fieldName: 'guestNameInputRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'guestNameInput' });
useRefToModel({ fieldName: 'inviteButtonRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'inviteButton' });
useRefToModel({ fieldName: 'threadNameInputRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'threadNameInput' });
useRefToModel({ fieldName: 'threadDescriptionInputRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'threadDescriptionInput' });
useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId' });
@@ -57,7 +57,7 @@ input.o_ThreadViewTopBar_guestNameInput {
width: 150px;
}
.o_ThreadViewTopbar_invitePopover .popover-body {
.o_ThreadViewTopbar_invitePopoverView .popover-body {
padding: 0; // cancel unwanted padding
}
@@ -58,14 +58,12 @@
</button>
</t>
<t t-if="threadViewTopBar.thread and threadViewTopBar.thread.hasInviteFeature">
<Popover popoverClass="'o_ThreadViewTopbar_invitePopover'" position="'bottom'">
<button class="o_ThreadViewTopbar_inviteButton o_ThreadViewTopbar_button" t-att-class="{ 'o-active': threadViewTopBar.threadView.channelInvitationForm.component }" title="Add users" t-on-click="threadViewTopBar.onClickInviteButton">
<i class="fa fa-lg fa-user-plus"/>
</button>
<t t-set="opened">
<ChannelInvitationForm localId="threadViewTopBar.threadView.channelInvitationForm.localId"/>
</t>
</Popover>
<button class="o_ThreadViewTopbar_inviteButton o_ThreadViewTopbar_button" t-att-class="{ 'o-active': threadViewTopBar.invitePopoverView }" title="Add users" t-on-click="threadViewTopBar.onClickInviteButton" t-ref="inviteButton">
<i class="fa fa-lg fa-user-plus"/>
</button>
<t t-if="threadViewTopBar.invitePopoverView">
<PopoverView class="o_ThreadViewTopbar_invitePopoverView" popoverViewLocalId="threadViewTopBar.invitePopoverView.localId"/>
</t>
</t>
<!-- FIXME: guests should be able to see members but there currently is no route for it, so hide it for now -->
<t t-if="!messaging.isCurrentUserGuest and threadViewTopBar.thread and threadViewTopBar.thread.hasMemberListFeature and threadViewTopBar.threadView.hasMemberList and !threadViewTopBar.threadView.isMemberListOpened">
@@ -21,13 +21,6 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
/**
* Closes this channel invitation form.
*/
close() {
this.component.trigger('o-popover-close');
}
/**
* Handles click on the "invite" button.
*
@@ -35,20 +28,12 @@ function factory(dependencies) {
*/
async onClickInvite(ev) {
if (this.thread.channel_type === 'chat') {
const channelData = await this.env.services.rpc(({
model: 'mail.channel',
method: 'create_group',
kwargs: {
partners_to: [...new Set([
this.messaging.currentPartner.id,
...this.thread.members.map(member => member.id),
...this.selectedPartners.map(partner => partner.id),
])],
},
}));
const channel = this.messaging.models['mail.thread'].insert(
this.messaging.models['mail.thread'].convertData(channelData)
);
const partners_to = [...new Set([
this.messaging.currentPartner.id,
...this.thread.members.map(member => member.id),
...this.selectedPartners.map(partner => partner.id),
])];
const channel = await this.messaging.models['mail.thread'].createGroupChat({ partners_to });
if (this.thread.rtc) {
/**
* if we were in a RTC call on the current thread, we move to the new group chat.
@@ -76,7 +61,7 @@ function factory(dependencies) {
searchTerm: "",
selectedPartners: unlinkAll(),
});
this.close();
this.delete();
}
/**
@@ -190,20 +175,6 @@ function factory(dependencies) {
return this.env._t("Invite to Channel");
}
/**
* @private
* @returns {mail.thread}
*/
_computeThread() {
if (this.threadView && this.threadView.thread) {
return link(this.threadView.thread);
}
if (this.discuss && this.discuss.mostRecentMeetingChannel) {
return link(this.discuss.mostRecentMeetingChannel);
}
return clear();
}
}
ChannelInvitationForm.fields = {
@@ -213,9 +184,6 @@ function factory(dependencies) {
* it is open to update the button active state.
*/
component: attr(),
discuss: one2one('mail.discuss', {
inverse: 'channelInvitationForm',
}),
/**
* Determines whether this search input needs to be focused.
*/
@@ -238,6 +206,10 @@ function factory(dependencies) {
inviteButtonText: attr({
compute: '_computeInviteButtonText',
}),
popoverView: one2one('mail.popover_view', {
inverse: 'channelInvitationForm',
isCausal: true,
}),
/**
* States the OWL ref of the "search" input of this channel invitation
* form. Useful to be able to focus it.
@@ -268,8 +240,8 @@ function factory(dependencies) {
* States the thread on which this list operates (if any).
*/
thread: many2one('mail.thread', {
compute: '_computeThread',
readonly: true,
related: 'threadView.thread',
required: true,
}),
/**
@@ -110,7 +110,9 @@ function factory(dependencies) {
makeActive(options) {
this.makeVisible();
this.unfold(options);
this.focus();
if ((options && options.focus !== undefined) ? options.focus : true) {
this.focus();
}
}
/**
@@ -98,12 +98,15 @@ function factory(dependencies) {
/**
* @param {mail.thread} thread
* @param {Object} [param1={}]
* @param {boolean} [param1.focus] if set, set focus the chat window
* to open.
* @param {boolean} [param1.isFolded=false]
* @param {boolean} [param1.makeActive=false]
* @param {boolean} [param1.notifyServer]
* @param {boolean} [param1.replaceNewMessage=false]
*/
openThread(thread, {
focus,
isFolded = false,
makeActive = false,
notifyServer,
@@ -131,7 +134,7 @@ function factory(dependencies) {
if (makeActive) {
// avoid double notify at this step, it will already be done at
// the end of the current method
chatWindow.makeActive({ notifyServer: false });
chatWindow.makeActive({ focus, notifyServer: false });
}
// Flux specific: notify server of chat window being opened.
if (notifyServer && !this.messaging.currentGuest) {
@@ -36,23 +36,6 @@ function factory(dependencies) {
this.update({ isOpen: false });
}
async createMeetingChannel() {
const channelData = await this.env.services.rpc({
model: 'mail.channel',
method: 'create_group',
kwargs: {
default_display_mode: 'video_full_screen',
partners_to: [this.messaging.currentPartner.id],
},
});
const channel = this.messaging.models['mail.thread'].insert(
this.messaging.models['mail.thread'].convertData(channelData)
);
this.update({ mostRecentMeetingChannel: link(channel) });
this.channelInvitationForm.update({ doFocusOnSearchInput: true });
this.channelInvitationForm.searchPartnersToInvite();
}
focus() {
if (this.threadView && this.threadView.composerView) {
this.threadView.composerView.update({ doFocus: true });
@@ -183,8 +166,17 @@ function factory(dependencies) {
*
* @param {MouseEvent} ev
*/
onClickStartAMeetingButton(ev) {
this.createMeetingChannel();
async onClickStartAMeetingButton(ev) {
const meetingChannel = await this.messaging.models['mail.thread'].createGroupChat({
default_display_mode: 'video_full_screen',
partners_to: [this.messaging.currentPartner.id],
});
meetingChannel.toggleCall({ startWithVideo: true });
await meetingChannel.open({ focus: false });
if (!meetingChannel.exists() || !this.threadView) {
return;
}
this.threadView.topbar.openInvitePopoverView();
}
/**
@@ -214,12 +206,16 @@ function factory(dependencies) {
* Opens the given thread in Discuss, and opens Discuss if necessary.
*
* @param {mail.thread} thread
* @param {Object} [param1={}]
* @param {Boolean} [param1.focus=true]
*/
async openThread(thread) {
async openThread(thread, { focus = true } = {}) {
this.update({
thread: link(thread),
});
this.focus();
if (focus) {
this.focus();
}
if (!this.isOpen) {
this.env.bus.trigger('do-action', {
action: 'mail.action_discuss',
@@ -278,22 +274,6 @@ function factory(dependencies) {
return this.addingChannelValue;
}
/**
* @private
* @returns {mail.channel_invitation_form}
*/
_computeChannelInvitationForm() {
if (!this.mostRecentMeetingChannel) {
return clear();
}
return insertAndReplace({
searchResultCount: clear(),
searchTerm: clear(),
selectablePartners: clear(),
selectedPartners: clear(),
});
}
/**
* @private
* @returns {boolean}
@@ -393,16 +373,6 @@ function factory(dependencies) {
inverse: 'discussAsChat',
isCausal: true,
}),
/**
* Determines the channel invitation form to use for the invitation
* to the most recent meeting channel.
*/
channelInvitationForm: one2one('mail.channel_invitation_form', {
compute: '_computeChannelInvitationForm',
inverse: 'discuss',
isCausal: true,
readonly: true,
}),
/**
* Determines whether `this.thread` should be displayed.
*/
@@ -450,11 +420,6 @@ function factory(dependencies) {
menu_id: attr({
default: null,
}),
/**
* The channel created last time the user clicked on the "Start a
* meeting" button.
*/
mostRecentMeetingChannel: one2one('mail.thread'),
/**
* Quick search input value in the discuss sidebar (desktop). Useful
* to filter channels and chats based on this input content.
@@ -462,6 +427,11 @@ function factory(dependencies) {
sidebarQuickSearchValue: attr({
default: "",
}),
/**
* States the OWL ref of the start a meeting button in sidebar.
* Useful to provide anchor for the invite popover positioning.
*/
startAMeetingButtonRef: attr(),
/**
* Determines the `mail.thread` that should be displayed by `this`.
*/
@@ -0,0 +1,115 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { attr, one2one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
function factory(dependencies) {
class PopoverView extends dependencies['mail.model'] {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
_created() {
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
document.addEventListener('click', this._onClickCaptureGlobal, true);
}
_willDelete() {
document.removeEventListener('click', this._onClickCaptureGlobal, true);
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
* @returns {owl.Ref}
*/
_computeAnchorRef() {
if (this.threadViewTopbarOwner) {
return this.threadViewTopbarOwner.inviteButtonRef;
}
return clear();
}
/**
* @private
* @returns {string}
*/
_computePosition() {
if (this.threadViewTopbarOwner) {
return 'bottom';
}
return clear();
}
/**
* Closes the popover when clicking outside, if appropriate.
*
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
if (!this.component || !this.component.el) {
return;
}
if (this.anchorRef && this.anchorRef.el && this.anchorRef.el.contains(ev.target)) {
return;
}
if (this.component.el.contains(ev.target)) {
return;
}
this.delete();
}
}
PopoverView.fields = {
/**
* HTML element that is used as anchor position for this popover view.
*/
anchorRef: attr({
compute: '_computeAnchorRef',
required: true,
}),
/**
* The record that represents the content inside the popover view.
*/
channelInvitationForm: one2one('mail.channel_invitation_form', {
inverse: 'popoverView',
isCausal: true,
readonly: true,
required: true,
}),
/**
* States the OWL component of this popover view.
*/
component: attr(),
/**
* Position of the popover view relative to its anchor point.
* Valid values: 'top', 'right', 'bottom', 'left'
*/
position: attr({
compute: '_computePosition',
default: 'top',
}),
/**
* If set, this popover view is owned by a thread view topbar record.
*/
threadViewTopbarOwner: one2one('mail.thread_view_topbar', {
inverse: 'invitePopoverView',
readonly: true,
}),
};
PopoverView.identifyingFields = ['threadViewTopbarOwner', 'channelInvitationForm'];
PopoverView.modelName = 'mail.popover_view';
return PopoverView;
}
registerNewModel('mail.popover_view', factory);
+28 -3
View File
@@ -316,6 +316,29 @@ function factory(dependencies) {
return data2;
}
/**
* Creates a new group chat with the provided partners.
*
* @param {Object} param0
* @param {number[]} param0.partners_to Ids of the partners to add as channel
* members.
* @param {boolean|string} param0.default_display_mode
* @returns {mail.thread} The newly created group chat.
*/
static async createGroupChat({ default_display_mode, partners_to }) {
const channelData = await this.env.services.rpc({
model: 'mail.channel',
method: 'create_group',
kwargs: {
default_display_mode,
partners_to,
},
});
return this.messaging.models['mail.thread'].insert(
this.messaging.models['mail.thread'].convertData(channelData)
);
}
/**
* Client-side ending of the call.
*/
@@ -971,8 +994,9 @@ function factory(dependencies) {
*
* @param {Object} [param0]
* @param {boolean} [param0.expanded=false]
* @param {boolean} [param0.focus=true]
*/
async open({ expanded = false } = {}) {
async open({ expanded = false, focus = true } = {}) {
const discuss = this.messaging.discuss;
// check if thread must be opened in form view
if (!['mail.box', 'mail.channel'].includes(this.model)) {
@@ -981,10 +1005,11 @@ function factory(dependencies) {
// both in chat window and as main document does not look
// good.
this.messaging.chatWindowManager.closeThread(this);
return this.messaging.openDocument({
await this.messaging.openDocument({
id: this.id,
model: this.model,
});
return;
}
}
// check if thread must be opened in discuss
@@ -993,7 +1018,7 @@ function factory(dependencies) {
(!device.isMobile && (discuss.isOpen || expanded)) ||
this.model === 'mail.box'
) {
return discuss.openThread(this);
return discuss.openThread(this, { focus });
}
// thread must be opened in chat window
return this.messaging.chatWindowManager.openThread(this, {
@@ -97,18 +97,6 @@ function factory(dependencies) {
/**
* @private
* @returns {mail.channel_invitation_form}
*/
_computeChannelInvitationForm() {
return (this.thread && this.thread.hasInviteFeature)
? insertAndReplace({
searchResultCount: clear(),
searchTerm: clear(),
selectablePartners: clear(),
selectedPartners: clear(),
})
: clear();
}
/**
* @private
@@ -361,10 +349,8 @@ function factory(dependencies) {
* Only applies if this thread is a channel.
*/
channelInvitationForm: one2one('mail.channel_invitation_form', {
compute: '_computeChannelInvitationForm',
inverse: 'threadView',
isCausal: true,
readonly: true,
}),
/**
* List of component hints. Hints contain information that help
@@ -2,7 +2,7 @@
import { registerNewModel } from '@mail/model/model_core';
import { attr, many2one, one2one } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import { clear, insertAndReplace, replace } from '@mail/model/model_field_command';
function factory(dependencies) {
@@ -75,12 +75,10 @@ function factory(dependencies) {
* @param {MouseEvent} ev
*/
onClickInviteButton(ev) {
if (this.threadView.channelInvitationForm.component) {
return;
}
if (!this.messaging.isCurrentUserGuest) {
this.threadView.channelInvitationForm.update({ doFocusOnSearchInput: true });
this.threadView.channelInvitationForm.searchPartnersToInvite();
if (this.invitePopoverView) {
this.update({ invitePopoverView: clear() });
} else {
this.openInvitePopoverView();
}
}
@@ -374,6 +372,23 @@ function factory(dependencies) {
this.update({ isMouseOverUserInfo: false });
}
/**
* Open the invite popover view in this thread view topbar.
*/
openInvitePopoverView() {
this.threadView.update({ channelInvitationForm: insertAndReplace() });
this.update({
invitePopoverView: insertAndReplace({
channelInvitationForm: replace(this.threadView.channelInvitationForm),
}),
});
if (this.messaging.isCurrentUserGuest) {
return;
}
this.threadView.channelInvitationForm.update({ doFocusOnSearchInput: true });
this.threadView.channelInvitationForm.searchPartnersToInvite();
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
@@ -594,6 +609,19 @@ function factory(dependencies) {
isEditingGuestName: attr({
default: false,
}),
/**
* States the OWL ref of the invite button.
* Useful to provide anchor for the invite popover positioning.
*/
inviteButtonRef: attr(),
/**
* If set, this is the record of invite button popover that is currently
* open in the topbar.
*/
invitePopoverView: one2one('mail.popover_view', {
isCausal: true,
inverse: 'threadViewTopbarOwner',
}),
/**
* Determines whether this thread is currently being renamed.
*/
@@ -229,8 +229,9 @@ export function usePosition(reference, options) {
const popperRef = popper ? useRef(popper) : useComponent();
const getReference = reference instanceof HTMLElement ? () => reference : reference;
const update = () => {
if (popperRef.el) {
reposition(getReference(), popperRef.el, options);
const ref = getReference();
if (popperRef.el && ref) {
reposition(ref, popperRef.el, options);
}
};
useEffect(update);