[IMP] discuss: add 'Start a meeting' feature

- Add a new stand-alone page "WelcomeView"
    - Redirect people who join using an invitation link to the WelcomeView
    - Add microphone/camera preview
    - Allow guests to choose a name from the WelcomeView
- Add "Start a meeting" button to the discuss sidebar
    - Create a new channel on click on the "Start a meeting" button
    - Display ChannelInvitationForm on click on the "Start a meeting"
button
    - Add a new field to `mail.channel` in order to define the display
mode to use for videos (e.g full screen) when joining a meeting

Task 2494829

closes odoo/odoo#75899

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Louis Wicket (wil)
2021-09-06 08:21:38 +00:00
committed by Sébastien Theys
parent 9f11a84d71
commit 401fc7efe9
27 changed files with 761 additions and 51 deletions
@@ -207,8 +207,8 @@ QUnit.test('basic rendering: sidebar', async function (assert) {
);
assert.strictEqual(
document.querySelectorAll(`.o_Discuss_sidebar .o_DiscussSidebar_separator`).length,
1,
"should have separator (between mailboxes and channels, but that's not tested)"
2,
"should have 2 separators (separating 'Start a meeting' button, mailboxes and channels, but that's not tested)"
);
assert.strictEqual(
document.querySelectorAll(`.o_DiscussSidebar_categoryChannel`).length,
@@ -3,6 +3,17 @@
<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>
<hr class="o_DiscussSidebar_separator"/>
<div class="o_DiscussSidebar_category o_DiscussSidebar_categoryMailbox">
<DiscussSidebarMailbox threadLocalId="messaging.inbox.localId"/>
<DiscussSidebarMailbox threadLocalId="messaging.starred.localId"/>
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
export class MediaPreview extends owl.Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'audioRef', modelName: 'mail.media_preview', propNameAsRecordLocalId: 'localId', refName: 'audio' });
useRefToModel({ fieldName: 'videoRef', modelName: 'mail.media_preview', propNameAsRecordLocalId: 'localId', refName: 'video' });
}
/**
* @returns {mail.media_preview}
*/
get mediaPreview() {
return this.messaging && this.messaging.models['mail.media_preview'].get(this.props.localId);
}
}
Object.assign(MediaPreview, {
props: { localId: String },
template: 'mail.MediaPreview',
});
registerMessagingComponent(MediaPreview);
@@ -0,0 +1,18 @@
.o_MediaPreview {
max-width: max-content;
}
.o_MediaPreview_mediaDevicesStatus {
@include o-position-absolute($bottom: 50%);
}
.o_MediaPreview_buttonsContainer {
@include o-position-absolute($bottom: 0);
}
.o_MediaPreview_button {
height: 4rem;
width: 4rem;
font-size: 1.5em;
line-height: 4rem;
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.MediaPreview" owl="1">
<div class="o_MediaPreview position-relative d-flex justify-content-center">
<video class="o_MediaPreview_videoDisplay shadow rounded bg-dark" height="480" width="640" autoplay="" t-ref="video"/>
<p t-if="mediaPreview.doesBrowserSupportMediaDevices and !mediaPreview.isVideoEnabled" class="o_MediaPreview_mediaDevicesStatus position-absolute text-light">
Camera is off
</p>
<p t-if="!mediaPreview.doesBrowserSupportMediaDevices" class="o_MediaPreview_mediaDevicesStatus text-light">
Your browser does not support videoconference
</p>
<div class="o_MediaPreview_buttonsContainer">
<button t-if="!mediaPreview.isMicrophoneEnabled" class="o_MediaPreview_enableMicrophoneButton o_MediaPreview_button btn btn-danger btn-lg rounded-circle p-0 m-3 fa fa-microphone-slash" t-on-click="mediaPreview.onClickEnableMicrophoneButton"/>
<button t-if="mediaPreview.isMicrophoneEnabled" class="o_MediaPreview_disableMicrophoneButton o_MediaPreview_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-microphone" t-on-click="mediaPreview.onClickDisableMicrophoneButton"/>
<button t-if="!mediaPreview.isVideoEnabled" class="o_MediaPreview_enableVideoButton o_MediaPreview_button btn btn-danger btn-lg p-0 m-3 rounded-circle fa fa-eye-slash" t-on-click="mediaPreview.onClickEnableVideoButton"/>
<button t-if="mediaPreview.isVideoEnabled" class="o_MediaPreview_disableVideoButton o_MediaPreview_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-video-camera" t-on-click="mediaPreview.onClickDisableVideoButton"/>
</div>
<audio class="o_MediaPreview_audioPlayer" autoplay="" t-ref="audio"/>
</div>
</t>
</templates>
@@ -5,7 +5,7 @@
<div class="o_RtcCallParticipantCard"
t-att-class="{
'o-is-talking': callParticipantCard and !callParticipantCard.isMinimized and callParticipantCard.isTalking,
'o-is-invitation': callParticipantCard and !!callParticipantCard.invitedPartner,
'o-is-invitation': callParticipantCard and callParticipantCard.isInvitation,
}"
t-on-contextmenu="_onContextMenu"
>
@@ -13,7 +13,7 @@
<div class="o_RtcCallParticipantCard_container"
t-att-class="{
'o-minimized': callParticipantCard.isMinimized,
'o-can-click': !!callParticipantCard.invitedPartner,
'o-can-click': !callParticipantCard.isInvitation,
}"
t-att-title="callParticipantCard.name"
t-att-aria-label="callParticipantCard.name"
@@ -28,7 +28,7 @@
<img alt="Avatar"
t-att-class="{
'o-is-talking': callParticipantCard.isTalking,
'o-is-invitation': !!callParticipantCard.invitedPartner,
'o-is-invitation': callParticipantCard.isInvitation,
}"
class="o_RtcCallParticipantCard_avatar_image rounded-circle"
t-att-src="callParticipantCard.avatarUrl"
@@ -0,0 +1,32 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model/use_update_to_model';
export class WelcomeView extends owl.Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'guestNameInputRef', modelName: 'mail.welcome_view', propNameAsRecordLocalId: 'localId', refName: 'guestNameInput' });
useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'mail.welcome_view', propNameAsRecordLocalId: 'localId' });
}
/**
* @returns {mail.welcome_view}
*/
get welcomeView() {
return this.messaging && this.messaging.models['mail.welcome_view'].get(this.props.localId);
}
}
Object.assign(WelcomeView, {
props: { localId: String },
template: 'mail.WelcomeView',
});
registerMessagingComponent(WelcomeView);
@@ -0,0 +1,14 @@
.o_WelcomeView_joinButton {
height: 4rem;
width: 4rem;
font-size: 2em;
line-height: 4rem;
}
.o_WelcomeView_guestNameInputLabel {
font-size: $font-size-lg;
}
.o_WelcomeView_loggedAsStatus {
font-size: $font-size-lg;
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.WelcomeView" owl="1">
<div class="o_WelcomeView h-100 d-flex flex-column justify-content-center align-items-center bg-light">
<h1 class="font-weight-light">You've been invited to a meeting!</h1>
<h2 class="m-5" t-esc="messaging.companyName"/>
<div class="d-flex justify-content-center">
<MediaPreview class="mr-5" localId="welcomeView.mediaPreview.localId"/>
<div class="d-flex flex-column justify-content-center">
<t t-if="messaging.currentGuest">
<label class="o_WelcomeView_guestNameInputLabel text-center" t-att-for="welcomeView.guestNameInputUniqueId">What's your name?</label>
<input class="form-control mb-3" type="text" placeholder="Your name" t-att-name="welcomeView.guestNameInputUniqueId" t-att-id="welcomeView.guestNameInputUniqueId" t-att-value="welcomeView.pendingGuestName" t-ref="guestNameInput" t-on-input="welcomeView.onInputGuestNameInput" t-on-keydown="welcomeView.onKeydownGuestNameInput"/>
</t>
<t t-if="messaging.currentUser">
<p class="o_WelcomeView_loggedAsStatus">Logged as <span t-esc="messaging.currentUser.nameOrDisplayName"/></p>
</t>
<button class="o_WelcomeView_joinButton btn btn-success btn-lg align-self-end p-0 rounded-circle shadow fa fa-phone" title="Join Channel" t-att-disabled="welcomeView.isJoinButtonDisabled" t-on-click="welcomeView.onClickJoinButton"/>
</div>
</div>
</div>
</t>
</templates>
@@ -190,6 +190,20 @@ 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 = {
@@ -199,6 +213,9 @@ 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.
*/
@@ -251,7 +268,8 @@ function factory(dependencies) {
* States the thread on which this list operates (if any).
*/
thread: many2one('mail.thread', {
related: 'threadView.thread',
compute: '_computeThread',
required: true,
}),
/**
* States the thread view on which this list operates (if any).
@@ -1,15 +1,25 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { attr, many2one, one2many, one2one } from '@mail/model/model_field';
import { attr, many2one, one2one } from '@mail/model/model_field';
import { clear, create, link, unlink, unlinkAll, update } from '@mail/model/model_field_command';
function factory(dependencies) {
class Discuss extends dependencies['mail.model'] {
/**
* @override
*/
_created() {
super._created();
// Bind necessary until OWL supports arrow function in handlers: https://github.com/odoo/owl/issues/876
this.onClickStartAMeetingButton = this.onClickStartAMeetingButton.bind(this);
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
clearIsAddingItem() {
this.update({
@@ -30,6 +40,23 @@ 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() {
this.update({ isDoFocus: true });
}
@@ -153,6 +180,15 @@ function factory(dependencies) {
this.update({ isAddingChannel: true });
}
/**
* Handles click on the "Start a meeting" button.
*
* @param {MouseEvent} ev
*/
onClickStartAMeetingButton(ev) {
this.createMeetingChannel();
}
/**
* Open thread from init active id. `initActiveId` is used to refer to
* a thread that we may not have full data yet, such as when messaging
@@ -265,6 +301,25 @@ function factory(dependencies) {
return this.addingChannelValue;
}
/**
* @private
* @returns {mail.channel_invitation_form}
*/
_computeChannelInvitationForm() {
if (!this.mostRecentMeetingChannel) {
return clear();
}
if (!this.channelInvitationForm) {
return create();
}
return update({
searchResultCount: clear(),
searchTerm: clear(),
selectablePartners: clear(),
selectedPartners: clear(),
});
}
/**
* @private
* @returns {boolean}
@@ -328,7 +383,6 @@ function factory(dependencies) {
return;
}
/**
* Only pinned threads are allowed in discuss.
*
@@ -389,6 +443,16 @@ function factory(dependencies) {
categoryChat: one2one('mail.discuss_sidebar_category', {
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.
*/
@@ -446,6 +510,11 @@ 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'),
/**
* The message that is currently selected as being replied to in Inbox.
* There is only one reply composer shown at a time, which depends on
@@ -0,0 +1,196 @@
/** @odoo-module **/
import { attr } from '@mail/model/model_field';
import { registerNewModel } from '@mail/model/model_core';
function factory(dependencies) {
class MediaPreview extends dependencies['mail.model'] {
/**
* @override
*/
_created() {
super._created();
// Bind necessary until OWL supports arrow function in handlers: https://github.com/odoo/owl/issues/876
this.onClickDisableMicrophoneButton = this.onClickDisableMicrophoneButton.bind(this);
this.onClickDisableVideoButton = this.onClickDisableVideoButton.bind(this);
this.onClickEnableMicrophoneButton = this.onClickEnableMicrophoneButton.bind(this);
this.onClickEnableVideoButton = this.onClickEnableVideoButton.bind(this);
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* Iterates tracks of the provided MediaStream, calling the `stop`
* method on each of them.
*
* @static
* @param {MediaStream} mediaStream
*/
static stopTracksOnMediaStream(mediaStream) {
for (const track of mediaStream.getTracks()) {
track.stop();
}
}
/**
* Stops recording user's microphone.
*/
disableMicrophone() {
this.audioRef.el.srcObject = null;
if (!this.audioStream) {
return;
}
this.messaging.models['mail.media_preview'].stopTracksOnMediaStream(this.audioStream);
this.update({ audioStream: null });
}
/**
* Stops recording user's video device.
*/
disableVideo() {
this.videoRef.el.srcObject = null;
if (!this.videoStream) {
return;
}
this.messaging.models['mail.media_preview'].stopTracksOnMediaStream(this.videoStream);
this.update({ videoStream: null });
}
/**
* Asks for access to the user's microphone if not granted yet, then
* starts recording and defines the resulting audio stream as the source
* of the audio element in order to play the audio feedback.
*/
async enableMicrophone() {
if (!this.doesBrowserSupportMediaDevices) {
return;
}
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.update({ audioStream });
this.audioRef.el.srcObject = this.audioStream;
}
/**
* Asks for access to the user's video device if not granted yet, then
* starts recording and defines the resulting video stream as the source
* of the video element in order to display the video feedback.
*/
async enableVideo() {
if (!this.doesBrowserSupportMediaDevices) {
return;
}
const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
this.update({ videoStream });
this.videoRef.el.srcObject = this.videoStream;
}
/**
* Handles click on the "disable microphone" button.
*/
onClickDisableMicrophoneButton() {
this.disableMicrophone();
}
/**
* Handles click on the "disable video" button.
*/
onClickDisableVideoButton() {
this.disableVideo();
}
/**
* Handles click on the "enable microphone" button.
*/
onClickEnableMicrophoneButton() {
this.enableMicrophone();
}
/**
* Handles click on the "enable video" button.
*/
onClickEnableVideoButton() {
this.enableVideo();
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
* @returns {boolean}
*/
_computeDoesBrowserSupportMediaDevices() {
return Boolean(
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia &&
window.MediaStream
);
}
/**
* @private
* @returns {boolean}
*/
_computeIsMicrophoneEnabled() {
return this.audioStream !== null;
}
/**
* @private
* @returns {boolean}
*/
_computeIsVideoEnabled() {
return this.videoStream !== null;
}
}
MediaPreview.fields = {
/**
* Ref to the audio element used for the audio feedback.
*/
audioRef: attr(),
/**
* The MediaStream from the microphone.
*
* Default set to null to be consistent with the default value of
* `HTMLMediaElement.srcObject`.
*/
audioStream: attr({
default: null,
}),
doesBrowserSupportMediaDevices: attr({
compute: '_computeDoesBrowserSupportMediaDevices',
}),
isMicrophoneEnabled: attr({
compute: '_computeIsMicrophoneEnabled',
}),
isVideoEnabled: attr({
compute: '_computeIsVideoEnabled',
}),
/**
* Ref to the video element used for the video feedback.
*/
videoRef: attr(),
/**
* The MediaStream from the camera.
*
* Default set to null to be consistent with the default value of
* `HTMLMediaElement.srcObject`.
*/
videoStream: attr({
default: null,
}),
};
MediaPreview.modelName = 'mail.media_preview';
return MediaPreview;
}
registerNewModel('mail.media_preview', factory);
@@ -261,6 +261,7 @@ function factory(dependencies) {
readonly: true,
}),
commands: one2many('mail.channel_command'),
companyName: attr(),
currentGuest: one2one('mail.guest'),
currentPartner: one2one('mail.partner'),
currentUser: one2one('mail.user'),
@@ -74,6 +74,7 @@ function factory(dependencies) {
async _init({
channels,
commands = [],
companyName,
current_partner,
currentGuest,
current_user_id,
@@ -113,6 +114,8 @@ function factory(dependencies) {
// failures after channels
this._initMailFailures(mail_failures);
discuss.update({ menu_id });
// company related data
this.messaging.update({ companyName });
}
/**
@@ -89,6 +89,8 @@ function factory(dependencies) {
return this._handleNotificationChannelRenamed(message.payload);
case 'mail.channel_update':
return this._handleNotificationChannelUpdate(message.payload);
case 'mail.guest_update':
return this.messaging.models['mail.guest'].insert(message.payload);
case 'mail.message_update':
return this.messaging.models['mail.message'].insert(message.payload);
case 'res.users_settings_changed':
@@ -97,6 +97,14 @@ function factory(dependencies) {
return callViewer && callViewer.isMinimized;
}
/**
* @private
* @returns {boolean}
*/
_computeIsInvitation() {
return Boolean(this.invitedPartner || this.invitedGuest);
}
/**
* @private
* @returns {boolean}
@@ -144,6 +152,13 @@ function factory(dependencies) {
* If set, this card represents an invitation of this partner to this call.
*/
invitedPartner: many2one('mail.partner'),
/**
* States whether this card is representing a person with a pending
* invitation.
*/
isInvitation: attr({
compute: '_computeIsInvitation'
}),
/**
* Determines if this card has to be displayed in a minimized form.
*/
@@ -175,6 +175,9 @@ function factory(dependencies) {
if ('avatarCacheKey' in data) {
data2.avatarCacheKey = data.avatarCacheKey;
}
if ('defaultDisplayMode' in data) {
data2.defaultDisplayMode = data.defaultDisplayMode;
}
if ('description' in data) {
data2.description = data.description;
}
@@ -2021,6 +2024,12 @@ function factory(dependencies) {
}),
creator: many2one('mail.user'),
custom_channel_name: attr(),
/**
* Determines the default display mode of this channel. Should contain
* either no value (to display the chat), or 'video_full_screen' to
* start a call in full screen.
*/
defaultDisplayMode: attr(),
/**
* States the description of this thread. Only applies to channels.
*/
@@ -0,0 +1,151 @@
/** @odoo-module **/
import { attr, one2one } from '@mail/model/model_field';
import { browser } from "@web/core/browser/browser";
import { create } from '@mail/model/model_field_command';
import { registerNewModel } from '@mail/model/model_core';
function factory(dependencies) {
const getNextGuestNameInputId = (function() {
let id = 0;
return () => ++id;
})();
class WelcomeView extends dependencies['mail.model'] {
/**
* @override
*/
_created() {
super._created();
// Bind necessary until OWL supports arrow function in handlers: https://github.com/odoo/owl/issues/876
this.onClickJoinButton = this.onClickJoinButton.bind(this);
this.onInputGuestNameInput = this.onInputGuestNameInput.bind(this);
this.onKeydownGuestNameInput = this.onKeydownGuestNameInput.bind(this);
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* @static
* @param {string} name
*/
static async updateGuestNameServerSide(name) {
await this.env.services.rpc({
route: '/mail/guest/update_name',
params: { name },
});
}
/**
* @param {MouseEvent} ev
*/
async onClickJoinButton(ev) {
this.updateGuestNameIfAnyThenJoinChannel();
}
/**
* Handles OWL update on this WelcomeView component.
*/
onComponentUpdate() {
this._handleFocus();
}
/**
* @param {KeyboardEvent} ev
*/
onInputGuestNameInput(ev) {
this._updateGuestNameWithInputValue();
}
/**
* @param {KeyboardEvent} ev
*/
onKeydownGuestNameInput(ev) {
if (ev.key === 'Enter') {
this.updateGuestNameIfAnyThenJoinChannel();
}
}
/**
* Redirects users to the related channel. If the current user is a
* guest, first updates their username with the value of guestNameInput.
*/
async updateGuestNameIfAnyThenJoinChannel() {
if (this.messaging.currentGuest) {
await this.messaging.models['mail.welcome_view'].updateGuestNameServerSide(this.pendingGuestName.trim());
}
browser.location.href = `/discuss/channel/${this.channelId}`;
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
*/
_handleFocus() {
if (this.isDoFocusGuestNameInput) {
if (!this.guestNameInputRef.el) {
return
}
this.update({ isDoFocusGuestNameInput: false });
this.guestNameInputRef.el.focus();
// place cursor at end of text
const { length } = (this.pendingGuestName || '');
this.guestNameInputRef.el.setSelectionRange(length, length);
}
}
/**
* @private
*/
_updateGuestNameWithInputValue() {
this.update({ pendingGuestName: this.guestNameInputRef.el.value });
}
/**
* @private
* @returns {boolean}
*/
_computeIsJoinButtonDisabled() {
return Boolean(this.messaging.currentGuest && !this.pendingGuestName.trim());
}
}
WelcomeView.fields = {
channelId: attr({
required: true,
}),
/**
* Used as value for `id`, `for`, and `name` attributes of the guest
* name input and its label. Necessary to ensure the uniqueness.
*/
guestNameInputUniqueId: attr({
default: `o_WelcomeView_guestNameInput_${getNextGuestNameInputId()}`,
}),
guestNameInputRef: attr(),
isDoFocusGuestNameInput: attr(),
isJoinButtonDisabled: attr({
compute: '_computeIsJoinButtonDisabled'
}),
mediaPreview: one2one('mail.media_preview', {
default: create(),
isCausal: true,
readonly: true,
required: true,
}),
pendingGuestName: attr(),
};
WelcomeView.modelName = 'mail.welcome_view';
return WelcomeView;
}
registerNewModel('mail.welcome_view', factory);
@@ -5,13 +5,13 @@ import { MessagingService } from '@mail/services/messaging/messaging';
import { getMessagingComponent } from '@mail/utils/messaging_component';
import { processTemplates } from '@web/core/assets';
import { registry } from "@web/core/registry";
import { registry } from '@web/core/registry';
import { makeEnv, startServices } from '@web/env';
import { session } from '@web/session';
import * as AbstractService from "web.AbstractService";
import * as AbstractService from 'web.AbstractService';
import { serviceRegistry as legacyServiceRegistry } from 'web.core';
import * as legacyEnv from "web.env";
import * as legacyEnv from 'web.env';
import {
makeLegacyCrashManagerService,
makeLegacyDialogMappingService,
@@ -19,21 +19,21 @@ import {
makeLegacyRpcService,
makeLegacySessionService,
mapLegacyEnvToWowlEnv,
} from "@web/legacy/utils";
import * as legacySession from "web.session";
} from '@web/legacy/utils';
import * as legacySession from 'web.session';
owl.Component.env = legacyEnv;
export async function boot() {
await owl.utils.whenReady();
owl.config.mode = owl.Component.env.isDebug() ? "dev" : "prod";
owl.config.mode = owl.Component.env.isDebug() ? 'dev' : 'prod';
AbstractService.prototype.deployServices(owl.Component.env);
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_rpc", makeLegacyRpcService(owl.Component.env));
serviceRegistry.add("legacy_session", makeLegacySessionService(owl.Component.env, legacySession));
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(owl.Component.env));
serviceRegistry.add("legacy_crash_manager", makeLegacyCrashManagerService(owl.Component.env));
serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(owl.Component.env));
const serviceRegistry = registry.category('services');
serviceRegistry.add('legacy_rpc', makeLegacyRpcService(owl.Component.env));
serviceRegistry.add('legacy_session', makeLegacySessionService(owl.Component.env, legacySession));
serviceRegistry.add('legacy_notification', makeLegacyNotificationService(owl.Component.env));
serviceRegistry.add('legacy_crash_manager', makeLegacyCrashManagerService(owl.Component.env));
serviceRegistry.add('legacy_dialog_mapping', makeLegacyDialogMappingService(owl.Component.env));
await legacySession.is_bound;
owl.Component.env.qweb.addTemplates(legacySession.owlTemplates);
Object.assign(odoo, {
@@ -76,7 +76,25 @@ export async function boot() {
threadViewLocalId: threadViewer.threadView.localId,
});
await threadViewComponent.mount(document.body);
if (threadViewer.thread.defaultDisplayMode === 'video_full_screen') {
await threadViewer.thread.toggleCall({ startWithVideo: true });
// TODO full screen not possible until the user actually makes an action on the page
}
}
return { createThreadViewFromChannelData };
async function createWelcomeView(channelId) {
const messaging = await owl.Component.env.services.messaging.get();
const WelcomeView = getMessagingComponent('WelcomeView');
const welcomeView = messaging.models['mail.welcome_view'].create({
channelId,
isDoFocusGuestNameInput: true,
pendingGuestName: messaging.currentGuest && messaging.currentGuest.name,
});
const welcomeViewComponent = new WelcomeView(null, {
localId: welcomeView.localId,
});
await welcomeViewComponent.mount(document.body);
}
return { createThreadViewFromChannelData, createWelcomeView };
}