[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:
committed by
Samuel Degueldre
parent
d5db1b9500
commit
021bc1bfd8
+56
-54
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user