[IMP] mail: merge welcome and thread views

This commit will merge welcome and thread views in the same route and
template. The aim is to make the user interacting with the page (by
landing them on the welcome view first) so that the browser allows the
page to play audio and to switch to full screen.

closes odoo/odoo#77381

X-original-commit: 97ed683e3e7cc6b340bb78c4faa01a703a573f32
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Louis Wicket (wil)
2021-09-30 17:11:46 +00:00
committed by Sébastien Theys
parent 57e714d8d1
commit 985b7f37d7
12 changed files with 400 additions and 173 deletions
+80 -37
View File
@@ -25,16 +25,39 @@ class DiscussController(http.Controller):
'/chat/<string:create_token>',
'/chat/<string:create_token>/<string:channel_name>',
], methods=['GET'], type='http', auth='public')
def discuss_channel_from_token(self, create_token, channel_name=None, **kwargs):
def discuss_channel_chat_from_token(self, create_token, channel_name=None, **kwargs):
return self._response_discuss_channel_from_token(create_token=create_token, channel_name=channel_name)
@http.route([
'/meet/<string:create_token>',
'/meet/<string:create_token>/<string:channel_name>',
], methods=['GET'], type='http', auth='public')
def discuss_channel_meet_from_token(self, create_token, channel_name=None, **kwargs):
return self._response_discuss_channel_from_token(create_token=create_token, channel_name=channel_name, default_display_mode='video_full_screen')
@http.route('/chat/<int:channel_id>/<string:invitation_token>', methods=['GET'], type='http', auth='public')
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs):
channel_sudo = request.env['mail.channel'].browse(channel_id).sudo().exists()
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token):
raise NotFound()
return self._response_discuss_channel_invitation(channel_sudo=channel_sudo)
@http.route('/discuss/channel/<int:channel_id>', methods=['GET'], type='http', auth='public')
def discuss_channel(self, channel_id, **kwargs):
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
return self._response_discuss_public_channel_template(channel_sudo=channel_partner_sudo.channel_id)
def _response_discuss_channel_from_token(self, create_token, channel_name=None, default_display_mode=False):
if not request.env['ir.config_parameter'].sudo().get_param('mail.chat_from_token'):
raise NotFound()
channel_sudo = request.env['mail.channel'].sudo().search([('uuid', '=', create_token)])
if not channel_sudo:
try:
channel_sudo = channel_sudo.create({
'uuid': create_token,
'default_display_mode': default_display_mode,
'name': channel_name or create_token,
'public': 'public',
'uuid': create_token,
})
except IntegrityError as e:
if e.pgcode != UNIQUE_VIOLATION:
@@ -43,50 +66,54 @@ class DiscussController(http.Controller):
# commit the current transaction and get the channel.
request.env.cr.commit()
channel_sudo = channel_sudo.search([('uuid', '=', create_token)])
return request.redirect(f'/chat/{channel_sudo.id}/{channel_sudo.uuid}')
return self._response_discuss_channel_invitation(channel_sudo=channel_sudo, is_channel_token_secret=False)
@http.route('/chat/<int:channel_id>/<string:invitation_token>', methods=['GET'], type='http', auth='public')
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs):
channel_sudo = request.env['mail.channel'].browse(int(channel_id)).sudo().exists()
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, invitation_token):
raise NotFound()
if channel_sudo.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=int(channel_id)):
return request.redirect(f'/discuss/channel/{channel_sudo.id}')
def _response_discuss_channel_invitation(self, channel_sudo, is_channel_token_secret=True):
if channel_sudo.channel_type == 'chat':
raise NotFound()
response = request.redirect(f'/discuss/channel/{channel_sudo.id}/welcome')
if not channel_sudo.env.user._is_public():
channel_sudo.add_members([channel_sudo.env.user.partner_id.id])
return response
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
if not guest:
guest = channel_sudo.env['mail.guest'].create({
'country_id': channel_sudo.env['res.country'].search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id,
'lang': get_lang(channel_sudo.env).code,
'name': _("Guest"),
'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request),
})
discuss_public_view_data = {
'isChannelTokenSecret': is_channel_token_secret,
}
add_guest_cookie = False
if not channel_sudo.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=channel_sudo.id):
if not channel_sudo.env.user._is_public():
channel_sudo.add_members([channel_sudo.env.user.partner_id.id])
else:
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
if guest:
channel_sudo.add_members(guest_ids=[guest.id])
else:
guest = channel_sudo.env['mail.guest'].create({
'country_id': channel_sudo.env['res.country'].search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id,
'lang': get_lang(channel_sudo.env).code,
'name': _("Guest"),
'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request),
})
add_guest_cookie = True
discuss_public_view_data.update({
'shouldAddGuestAsMemberOnJoin': True,
'shouldDisplayWelcomeViewInitially': True,
})
channel_sudo = channel_sudo.with_context(guest=guest)
response = self._response_discuss_public_channel_template(channel_sudo=channel_sudo, discuss_public_view_data=discuss_public_view_data)
if add_guest_cookie:
# Discuss Guest ID: every route in this file will make use of it to authenticate
# the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`.
expiration_date = datetime.now() + timedelta(days=365)
response.set_cookie(guest._cookie_name, f"{guest.id}{guest._cookie_separator}{guest.access_token}", httponly=True, expires=expiration_date)
channel_sudo.add_members(guest_ids=[guest.id])
return response
@http.route('/discuss/channel/<int:channel_id>', methods=['GET'], type='http', auth='public')
def discuss_channel(self, channel_id, **kwargs):
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
def _response_discuss_public_channel_template(self, channel_sudo, discuss_public_view_data=None):
discuss_public_view_data = discuss_public_view_data or {}
return request.render('mail.discuss_public_channel_template', {
'channel_sudo': channel_partner_sudo.channel_id,
'session_info': channel_partner_sudo.env['ir.http'].session_info(),
})
@http.route('/discuss/channel/<int:channel_id>/welcome', methods=['GET'], type='http', auth='public')
def discuss_channel_welcome(self, channel_id):
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
return request.render('mail.discuss_public_welcome_template', {
'channel_sudo': channel_partner_sudo.channel_id,
'session_info': channel_partner_sudo.env['ir.http'].session_info(),
'data': {
'channelData': channel_sudo.channel_info()[0],
'discussPublicViewData': dict({
'channel': [('insert', {'id': channel_sudo.id, 'model': 'mail.channel'})],
'shouldDisplayWelcomeViewInitially': channel_sudo.default_display_mode == 'video_full_screen',
}, **discuss_public_view_data),
},
'session_info': channel_sudo.env['ir.http'].session_info(),
})
# --------------------------------------------------------------------------
@@ -303,6 +330,22 @@ class DiscussController(http.Controller):
# Channel API
# --------------------------------------------------------------------------
@http.route('/mail/channel/add_guest_as_member', methods=['POST'], type='json', auth='public')
def mail_channel_add_guest_as_member(self, channel_id, channel_uuid, **kwargs):
channel_sudo = request.env['mail.channel'].browse(int(channel_id)).sudo().exists()
if not channel_sudo or not channel_sudo.uuid or not consteq(channel_sudo.uuid, channel_uuid):
raise NotFound()
if channel_sudo.channel_type == 'chat':
raise NotFound()
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
# Only guests should take this route.
if not guest:
raise NotFound()
channel_partner = channel_sudo.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=channel_id)
# Do not add the guest to channel members if they are already member.
if not channel_partner:
channel_sudo.add_members(guest_ids=[guest.id])
@http.route('/mail/channel/messages', methods=['POST'], type='json', auth='public')
def mail_channel_messages(self, channel_id, max_id=None, min_id=None, limit=30, **kwargs):
channel_partner_sudo = request.env['mail.channel.partner']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
@@ -434,7 +477,7 @@ class DiscussController(http.Controller):
if session and session.guest_id == guest:
session._update_and_broadcast(values)
return
raise NotFound()
return
session = request.env['mail.channel.rtc.session'].sudo().browse(int(session_id)).exists()
if session and session.partner_id == request.env.user.partner_id:
session._update_and_broadcast(values)
@@ -0,0 +1,22 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
class DiscussPublicView extends Component {
/**
* @returns {mail.discuss_public_view}
*/
get discussPublicView() {
return this.messaging && this.messaging.models['mail.discuss_public_view'].get(this.props.localId);
}
}
Object.assign(DiscussPublicView, {
props: { localId: String },
template: 'mail.DiscussPublicView',
});
registerMessagingComponent(DiscussPublicView);
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.DiscussPublicView" owl="1">
<div class="o_DiscussPublicView d-flex flex-column h-100">
<t t-if="discussPublicView">
<ThreadView t-if="discussPublicView.threadView"
composerAttachmentsDetailsMode="'card'"
hasComposer="true"
hasComposerThreadTyping="true"
hasSquashCloseMessages="true"
threadViewLocalId="discussPublicView.threadView.localId"
/>
<WelcomeView t-if="discussPublicView.welcomeView"
localId="discussPublicView.welcomeView.localId"
/>
</t>
</div>
</t>
</templates>
@@ -3,20 +3,22 @@
<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"/>
<t t-if="mediaPreview">
<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"/>
</t>
</div>
</t>
@@ -3,21 +3,23 @@
<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"/>
<t t-if="welcomeView">
<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>
</div>
</t>
@@ -0,0 +1,106 @@
/** @odoo-module **/
import { attr, one2one } from '@mail/model/model_field';
import { registerNewModel } from '@mail/model/model_core';
import { clear, create, link } from '@mail/model/model_field_command';
function factory(dependencies) {
class DiscussPublicView extends dependencies['mail.model'] {
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
/**
* Creates and displays the thread view and clears the welcome view.
*/
async switchToThreadView() {
this.update({
threadViewer: create({
extraClass: 'flex-grow-1',
hasMemberList: true,
hasThreadView: true,
hasTopbar: true,
thread: link(this.channel),
}),
welcomeView: clear(),
});
if (this.isChannelTokenSecret) {
// Change the URL to avoid leaking the invitation link.
window.history.replaceState(window.history.state, null, `/discuss/channel/${this.channel.id}`);
}
if (this.channel.defaultDisplayMode === 'video_full_screen') {
await this.channel.toggleCall({ startWithVideo: true });
await this.threadView.rtcCallViewer.activateFullScreen();
}
}
/**
* Creates and displays the welcome view and clears the thread viewer.
*/
switchToWelcomeView() {
this.update({
threadViewer: clear(),
welcomeView: create({
channel: link(this.channel),
isDoFocusGuestNameInput: true,
originalGuestName: this.messaging.currentGuest && this.messaging.currentGuest.name,
pendingGuestName: this.messaging.currentGuest && this.messaging.currentGuest.name,
}),
});
if (this.channel.defaultDisplayMode === 'video_full_screen') {
this.welcomeView.mediaPreview.enableMicrophone();
this.welcomeView.mediaPreview.enableVideo();
}
}
}
DiscussPublicView.fields = {
/**
* States the channel linked to this discuss public view.
*/
channel: one2one('mail.thread', {
readonly: true,
required: true,
}),
isChannelTokenSecret: attr({
default: true,
}),
shouldAddGuestAsMemberOnJoin: attr({
default: false,
readonly: true,
}),
shouldDisplayWelcomeViewInitially: attr({
default: false,
readonly: true,
}),
/**
* States the thread view linked to this discuss public view.
*/
threadView: one2one('mail.thread_view', {
readonly: true,
related: 'threadViewer.threadView',
}),
/**
* States the thread viewer linked to this discuss public view.
*/
threadViewer: one2one('mail.thread_viewer', {
isCausal: true,
}),
/**
* States the welcome view linked to this discuss public view.
*/
welcomeView: one2one('mail.welcome_view', {
inverse: 'discussPublicView',
isCausal: true,
}),
};
DiscussPublicView.modelName = 'mail.discuss_public_view';
return DiscussPublicView;
}
registerNewModel('mail.discuss_public_view', factory);
@@ -69,9 +69,13 @@ function factory(dependencies) {
if (!this.doesBrowserSupportMediaDevices) {
return;
}
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.update({ audioStream });
this.audioRef.el.srcObject = this.audioStream;
try {
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.update({ audioStream });
this.audioRef.el.srcObject = this.audioStream;
} catch {
// TODO: display popup asking the user to re-enable their mic
}
}
/**
@@ -83,9 +87,13 @@ function factory(dependencies) {
if (!this.doesBrowserSupportMediaDevices) {
return;
}
const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
this.update({ videoStream });
this.videoRef.el.srcObject = this.videoStream;
try {
const videoStream = await navigator.mediaDevices.getUserMedia({ video: true });
this.update({ videoStream });
this.videoRef.el.srcObject = this.videoStream;
} catch {
// TODO: display popup asking the user to re-enable their camera
}
}
/**
@@ -171,6 +171,9 @@ function factory(dependencies) {
}
this.update(data);
this._debounce(async () => {
if (!this.exists()) {
return;
}
await this.async(() => {
this.env.services.rpc(
{
@@ -2079,10 +2079,10 @@ function factory(dependencies) {
futureActivities: one2many('mail.activity', {
compute: '_computeFutureActivities',
}),
guestMembers: many2many('mail.guest'),
group_based_subscription: attr({
default: false,
}),
guestMembers: many2many('mail.guest'),
/**
* States whether `this` has activities (`mail.activity.mixin` server side).
*/
@@ -1,13 +1,12 @@
/** @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() {
const getNextGuestNameInputId = (function () {
let id = 0;
return () => ++id;
})();
@@ -31,9 +30,9 @@ function factory(dependencies) {
/**
* @static
* @param {string} name
* @param {string} name
*/
static async updateGuestNameServerSide(name) {
static async performRpcGuestUpdateName(name) {
await this.env.services.rpc({
route: '/mail/guest/update_name',
params: { name },
@@ -41,10 +40,24 @@ function factory(dependencies) {
}
/**
* @param {MouseEvent} ev
* Updates guest if needed then displays the thread view instead of the
* welcome view.
*/
async onClickJoinButton(ev) {
this.updateGuestNameIfAnyThenJoinChannel();
async joinChannel() {
if (this.hasGuestNameChanged) {
await this.messaging.models['mail.welcome_view'].performRpcGuestUpdateName(this.pendingGuestName.trim());
}
if (this.discussPublicView.shouldAddGuestAsMemberOnJoin) {
await this.performRpcAddGuestAsMember();
}
this.discussPublicView.switchToThreadView();
}
/**
* @param {MouseEvent} ev
*/
onClickJoinButton(ev) {
this.joinChannel();
}
/**
@@ -55,43 +68,70 @@ function factory(dependencies) {
}
/**
* @param {KeyboardEvent} ev
* @param {KeyboardEvent} ev
*/
onInputGuestNameInput(ev) {
this._updateGuestNameWithInputValue();
}
/**
* @param {KeyboardEvent} ev
* @param {KeyboardEvent} ev
*/
onKeydownGuestNameInput(ev) {
if (ev.key === 'Enter') {
this.updateGuestNameIfAnyThenJoinChannel();
this.joinChannel();
}
}
/**
* Redirects users to the related channel. If the current user is a
* guest, first updates their username with the value of guestNameInput.
* Adds the current guest to members of the channel linked to this
* welcome view.
*/
async updateGuestNameIfAnyThenJoinChannel() {
if (this.messaging.currentGuest) {
await this.messaging.models['mail.welcome_view'].updateGuestNameServerSide(this.pendingGuestName.trim());
}
browser.location.href = `/discuss/channel/${this.channel.id}`;
async performRpcAddGuestAsMember() {
await this.env.services.rpc({
route: '/mail/channel/add_guest_as_member',
params: {
channel_id: this.channel.id,
channel_uuid: this.channel.uuid,
},
});
}
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
/**
* @private
* @returns {string}
*/
_computeGuestNameInputUniqueId() {
return `o_WelcomeView_guestNameInput_${getNextGuestNameInputId()}`;
}
/**
* @private
* @returns {boolean}
*/
_computeHasGuestNameChanged() {
return Boolean(this.messaging.currentGuest && this.originalGuestName !== this.pendingGuestName);
}
/**
* @private
* @returns {boolean}
*/
_computeIsJoinButtonDisabled() {
return Boolean(this.messaging.currentGuest && this.pendingGuestName.trim() === '');
}
/**
* @private
*/
_handleFocus() {
if (this.isDoFocusGuestNameInput) {
if (!this.guestNameInputRef.el) {
return
return;
}
this.update({ isDoFocusGuestNameInput: false });
this.guestNameInputRef.el.focus();
@@ -102,40 +142,59 @@ function factory(dependencies) {
}
/**
* Updates `pendingGuestName` with the value of the input element
* referred by `guestNameInputRef`.
*
* @private
*/
_updateGuestNameWithInputValue() {
this.update({ pendingGuestName: this.guestNameInputRef.el.value });
}
/**
* @private
* @returns {boolean}
*/
_computeIsJoinButtonDisabled() {
return Boolean(this.messaging.currentGuest && !this.pendingGuestName.trim());
}
}
WelcomeView.fields = {
/**
* The channel to redirect to once the user clicks on the 'joinButton'.
* States the channel to redirect to once the user clicks on the
* 'joinButton'.
*/
channel: one2one('mail.thread', {
readonly: true,
required: true,
}),
/**
* Used as a value for `id`, `for`, and `name` attributes of the guest
* name input and its label. Necessary to ensure the uniqueness.
* States discuss public view on which this welcome view is displayed.
*/
guestNameInputUniqueId: attr({
default: `o_WelcomeView_guestNameInput_${getNextGuestNameInputId()}`,
discussPublicView: one2one('mail.discuss_public_view', {
inverse: 'welcomeView',
readonly: true,
required: true,
}),
/**
* Ref the to input element containing the 'pendingGuestName'.
* States the OWL ref the to input element containing the
* 'pendingGuestName'.
*/
guestNameInputRef: attr(),
/**
* States the value to use for `id`, `for`, and `name` attributes of
* the guest name input and its label.
*
* Necessary to ensure the uniqueness.
*/
guestNameInputUniqueId: attr({
compute: '_computeGuestNameInputUniqueId',
readonly: true,
}),
/**
* Determines whether the guest's name has been updated.
*
* Useful to determine whether a RPC should be done to update the name
* server side.
*/
hasGuestNameChanged: attr({
compute: '_computeHasGuestNameChanged',
readonly: true,
}),
/**
* Determines whether the 'guestNameInput' should be focused the next
* time the component is updated.
@@ -143,7 +202,7 @@ function factory(dependencies) {
isDoFocusGuestNameInput: attr(),
/**
* Determines whether the 'joinButton' is disabled.
*
*
* Shall be disabled when 'pendingGuestName' is an empty string while
* the current user is a guest.
*/
@@ -151,7 +210,7 @@ function factory(dependencies) {
compute: '_computeIsJoinButtonDisabled'
}),
/**
* The MediaPreview linked to the current WelcomeView.
* States the media preview embedded in this welcome view.
*/
mediaPreview: one2one('mail.media_preview', {
default: create(),
@@ -160,8 +219,14 @@ function factory(dependencies) {
required: true,
}),
/**
* The value of the 'guestNameInput'.
*
* States the name the guest had when landing on the welcome view.
*
* Useful to determine whether the name has changed.
*/
originalGuestName: attr(),
/**
* Determines the value of the 'guestNameInput'.
*
* Will be used to update the current guest's name when joining the
* channel by clicking on the 'joinButton'.
*/
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { insert, link } from '@mail/model/model_field_command';
import { data } from 'mail.discuss_public_channel_template';
import { MessagingService } from '@mail/services/messaging/messaging';
import { getMessagingComponent } from '@mail/utils/messaging_component';
@@ -25,7 +26,7 @@ import * as legacySession from 'web.session';
owl.Component.env = legacyEnv;
export async function boot() {
(async function boot() {
await owl.utils.whenReady();
owl.config.mode = owl.Component.env.isDebug() ? 'dev' : 'prod';
AbstractService.prototype.deployServices(owl.Component.env);
@@ -60,51 +61,21 @@ export async function boot() {
},
}));
await owl.mount(MainComponentsContainer, { env, target: document.body });
createAndMountDiscussPublicView();
})();
async function createThreadViewFromChannelData(channelData) {
const messaging = await owl.Component.env.services.messaging.get();
const threadViewer = messaging.models['mail.thread_viewer'].create({
extraClass: 'flex-grow-1',
hasMemberList: true,
hasThreadView: true,
hasTopbar: true,
thread: insert(messaging.models['mail.thread'].convertData(channelData)),
});
const ThreadView = getMessagingComponent('ThreadView');
const threadViewComponent = new ThreadView(null, {
composerAttachmentsDetailsMode: 'card',
hasComposer: true,
hasComposerThreadTyping: true,
hasSquashCloseMessages: true,
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
}
async function createAndMountDiscussPublicView() {
const messaging = await owl.Component.env.services.messaging.get();
messaging.models['mail.thread'].insert(messaging.models['mail.thread'].convertData(data.channelData));
const discussPublicView = messaging.models['mail.discuss_public_view'].create(data.discussPublicViewData);
if (discussPublicView.shouldDisplayWelcomeViewInitially) {
discussPublicView.switchToWelcomeView();
} else {
discussPublicView.switchToThreadView();
}
async function createWelcomeView(channelData) {
const messaging = await owl.Component.env.services.messaging.get();
const channel = messaging.models['mail.thread'].insert(
messaging.models['mail.thread'].convertData(channelData)
);
const WelcomeView = getMessagingComponent('WelcomeView');
const welcomeView = messaging.models['mail.welcome_view'].create({
channel: link(channel),
isDoFocusGuestNameInput: true,
pendingGuestName: messaging.currentGuest && messaging.currentGuest.name,
});
const welcomeViewComponent = new WelcomeView(null, {
localId: welcomeView.localId,
});
await welcomeViewComponent.mount(document.body);
if (welcomeView.channel.defaultDisplayMode === 'video_full_screen') {
welcomeView.mediaPreview.enableMicrophone();
welcomeView.mediaPreview.enableVideo();
}
}
return { createThreadViewFromChannelData, createWelcomeView };
const DiscussPublicView = getMessagingComponent('DiscussPublicView');
const discussPublicViewComponent = new DiscussPublicView(null, {
localId: discussPublicView.localId,
});
await discussPublicViewComponent.mount(document.body);
}
+4 -18
View File
@@ -38,24 +38,10 @@
<t t-call="mail.discuss_public_layout">
<t t-set="head">
<script>
odoo.define('mail.discuss_public_boot_channel', async function(require) {
const { boot } = require('@mail/public/discuss_public_boot');
const { createThreadViewFromChannelData } = await boot();
createThreadViewFromChannelData(<t t-out="json.dumps(channel_sudo.channel_info()[0])"/>);
});
</script>
</t>
</t>
</template>
<template id="mail.discuss_public_welcome_template" name="Discuss Public Welcome Template">
<t t-call="mail.discuss_public_layout">
<t t-set="head">
<script>
odoo.define('mail.discuss_public_boot_welcome_view', async function(require) {
const { boot } = require('@mail/public/discuss_public_boot');
const { createWelcomeView } = await boot();
createWelcomeView(<t t-out="json.dumps(channel_sudo.channel_info()[0])"/>);
odoo.define('mail.discuss_public_channel_template', function() {
return {
data: <t t-out="json.dumps(data)"/>,
};
});
</script>
</t>