[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:
committed by
Sébastien Theys
parent
9f11a84d71
commit
401fc7efe9
@@ -23,21 +23,21 @@ class DiscussController(http.Controller):
|
||||
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()
|
||||
response = request.redirect(f'/discuss/channel/{channel_sudo.id}')
|
||||
if request.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=int(channel_id)):
|
||||
return response
|
||||
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}')
|
||||
if channel_sudo.channel_type == 'chat':
|
||||
raise NotFound()
|
||||
if request.session.uid:
|
||||
channel_sudo.add_members([request.env.user.partner_id.id])
|
||||
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 = request.env['mail.guest']._get_guest_from_request(request)
|
||||
guest = channel_sudo.env['mail.guest']._get_guest_from_request(request)
|
||||
if not guest:
|
||||
guest = request.env['mail.guest'].sudo().create({
|
||||
'country_id': request.env['res.country'].sudo().search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id,
|
||||
'lang': get_lang(request.env).code,
|
||||
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': request.env['mail.guest']._get_timezone_from_request(request),
|
||||
'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request),
|
||||
})
|
||||
# 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`.
|
||||
@@ -54,6 +54,14 @@ class DiscussController(http.Controller):
|
||||
'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(),
|
||||
})
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Semi-Static Content (GET requests with possible cache)
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -92,7 +100,7 @@ class DiscussController(http.Controller):
|
||||
|
||||
@http.route('/mail/init_messaging', methods=['POST'], type='json', auth='public')
|
||||
def mail_init_messaging(self, **kwargs):
|
||||
if request.session.uid:
|
||||
if not request.env.user.sudo()._is_public():
|
||||
return request.env.user._init_messaging()
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
if guest:
|
||||
@@ -304,7 +312,7 @@ class DiscussController(http.Controller):
|
||||
|
||||
@http.route('/mail/rtc/session/notify_call_members', methods=['POST'], type="json", auth="public")
|
||||
def session_call_notify(self, sender_session_id, target_session_ids=None, content=None):
|
||||
""" Sends content to other session of the same channel, only works of the user is the user of that session.
|
||||
""" Sends content to other session of the same channel, only works if the user is the user of that session.
|
||||
This is used to send peer to peer information between sessions.
|
||||
|
||||
:param int sender_session_id: id of the session from which the content is sent
|
||||
@@ -382,3 +390,14 @@ class DiscussController(http.Controller):
|
||||
('Cache-Control', 'max-age=%s' % http.STATIC_CACHE),
|
||||
]
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Guest API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/guest/update_name', methods=['POST'], type='json', auth='public')
|
||||
def mail_guest_update_name(self, name):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
if not guest:
|
||||
raise NotFound()
|
||||
guest.sudo()._update_name(name)
|
||||
|
||||
@@ -18,7 +18,7 @@ class IrHttp(models.AbstractModel):
|
||||
assets_discuss_public_hash = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle='mail.assets_discuss_public')
|
||||
result['cache_hashes']['assets_discuss_public'] = assets_discuss_public_hash
|
||||
guest = self.env.context.get('guest')
|
||||
if guest:
|
||||
if self.env.user._is_public() and guest:
|
||||
user_context = {'lang': guest.lang}
|
||||
mods = odoo.conf.server_wide_modules or []
|
||||
lang = user_context.get("lang")
|
||||
|
||||
@@ -58,6 +58,7 @@ class Channel(models.Model):
|
||||
('group', 'Group')],
|
||||
string='Channel Type', default='channel', help="Chat is private and unique between 2 persons. Group is private among invited persons. Channel can be freely joined (depending on its configuration).")
|
||||
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat')
|
||||
default_display_mode = fields.Selection(string="Default Display Mode", selection=[('video_full_screen', "Full screen video")], help="Determines how the channel will be displayed by default when opening it from its invitation link. No value means display text (no voice/video).")
|
||||
description = fields.Text('Description')
|
||||
image_128 = fields.Image("Image", max_width=128, max_height=128)
|
||||
avatar_128 = fields.Image("Avatar", max_width=128, max_height=128, compute='_compute_avatar_128')
|
||||
@@ -326,7 +327,7 @@ class Channel(models.Model):
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', channel_partner.partner_id.id), {
|
||||
'type': 'mail.channel_joined',
|
||||
'payload': {
|
||||
'channel': channel_partner.channel_id.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).channel_info()[0],
|
||||
'channel': channel_partner.channel_id.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).sudo().channel_info()[0],
|
||||
'invited_by_user_id': self.env.user.id,
|
||||
},
|
||||
})
|
||||
@@ -427,15 +428,19 @@ class Channel(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
guest = self.env.context.get('guest')
|
||||
partner = None if guest else self.env.user.partner_id
|
||||
if self.env.user._is_public():
|
||||
guest = self.env.context.get('guest')
|
||||
partner = self.env['res.partner']
|
||||
else:
|
||||
guest = self.env['mail.guest']
|
||||
partner = self.env.user.partner_id
|
||||
|
||||
def is_current_user(r):
|
||||
return (guest and r.guest_id == guest) or r.partner_id == partner
|
||||
|
||||
current_rtc_session = self.rtc_session_ids.filtered(is_current_user)
|
||||
if not current_rtc_session:
|
||||
return
|
||||
return [], []
|
||||
|
||||
current_sessions_channel_partners = self.rtc_session_ids.channel_partner_id
|
||||
notifications = []
|
||||
@@ -504,15 +509,14 @@ class Channel(models.Model):
|
||||
the channel if necessary.
|
||||
:param bool joining : true if joining the call, false if leaving.
|
||||
"""
|
||||
guest = self.env.context.get('guest')
|
||||
partner = None if guest else self.env.user.partner_id
|
||||
new_session_id = None
|
||||
if partner:
|
||||
domain = [('partner_id', '=', partner.id)]
|
||||
if self.env.user._is_public():
|
||||
guest = self.env.context.get('guest')
|
||||
partner = self.env['res.partner']
|
||||
else:
|
||||
domain = [('guest_id', '=', guest.id)]
|
||||
current_channel_partner = self.env['mail.channel.partner'].search(domain, limit=1)
|
||||
|
||||
guest = self.env['mail.guest']
|
||||
partner = self.env.user.partner_id
|
||||
new_session_id = None
|
||||
current_channel_partner = self.env['mail.channel.partner'].search([('channel_id', '=', self.id), ('partner_id', '=', partner.id), ('guest_id', '=', guest.id)], limit=1)
|
||||
if not current_channel_partner:
|
||||
return
|
||||
current_channel_partner._remove_rtc_invitation()
|
||||
@@ -788,6 +792,7 @@ class Channel(models.Model):
|
||||
'avatarCacheKey': channel._get_avatar_cache_key(),
|
||||
'id': channel.id,
|
||||
'name': channel.name,
|
||||
'defaultDisplayMode': channel.default_display_mode,
|
||||
'description': channel.description,
|
||||
'uuid': channel.uuid,
|
||||
'state': 'open',
|
||||
@@ -1126,7 +1131,7 @@ class Channel(models.Model):
|
||||
return channel_info
|
||||
|
||||
@api.model
|
||||
def create_group(self, partners_to):
|
||||
def create_group(self, partners_to, default_display_mode=False):
|
||||
""" Create a group channel.
|
||||
:param partners_to : list of res.partner ids to add to the conversation
|
||||
:returns: channel_info of the created channel
|
||||
@@ -1135,6 +1140,7 @@ class Channel(models.Model):
|
||||
channel = self.create({
|
||||
'channel_last_seen_partner_ids': [Command.create({'partner_id': partner_id}) for partner_id in partners_to],
|
||||
'channel_type': 'group',
|
||||
'default_display_mode': default_display_mode,
|
||||
'name': '', # default name is computed client side from the list of members
|
||||
'public': 'private',
|
||||
})
|
||||
|
||||
@@ -5,8 +5,9 @@ import pytz
|
||||
import uuid
|
||||
|
||||
from odoo.tools import consteq
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.base.models.res_partner import _tz_get
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MailGuest(models.Model):
|
||||
@@ -48,6 +49,23 @@ class MailGuest(models.Model):
|
||||
timezone = request.httprequest.cookies.get('tz')
|
||||
return timezone if timezone in pytz.all_timezones else False
|
||||
|
||||
def _update_name(self, name):
|
||||
self.ensure_one()
|
||||
name = name.strip()
|
||||
if name == "":
|
||||
raise UserError(_("Guest's name cannot be empty."))
|
||||
self.name = name
|
||||
self.env['bus.bus'].sendmany([(
|
||||
(self._cr.dbname, 'mail.channel', channel.id),
|
||||
{
|
||||
'type': 'mail.guest_update',
|
||||
'payload': {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
},
|
||||
}
|
||||
) for channel in self.channel_ids])
|
||||
|
||||
def _update_timezone(self, timezone):
|
||||
query = """
|
||||
UPDATE mail_guest
|
||||
@@ -64,6 +82,7 @@ class MailGuest(models.Model):
|
||||
partner_root = self.env.ref('base.partner_root')
|
||||
return {
|
||||
'channels': self.channel_ids.channel_info(),
|
||||
'companyName': self.env.company.name,
|
||||
'currentGuest': {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
|
||||
@@ -1802,7 +1802,7 @@ class MailThread(models.AbstractModel):
|
||||
record_name = record_name or self.display_name
|
||||
|
||||
# Find the message's author
|
||||
if 'guest' in self.env.context:
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
author_guest_id = self.env.context['guest'].id
|
||||
author_id, email_from = False, False
|
||||
else:
|
||||
|
||||
@@ -94,6 +94,7 @@ class Users(models.Model):
|
||||
partner_root = self.env.ref('base.partner_root')
|
||||
values = {
|
||||
'channels': self.partner_id._get_channels_as_member().channel_info(),
|
||||
'companyName': self.env.company.name,
|
||||
'currentGuest': False,
|
||||
'current_partner': self.partner_id.mail_partner_format().get(self.partner_id),
|
||||
'current_user_id': self.id,
|
||||
|
||||
@@ -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>
|
||||
+3
-3
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="mail.discuss_public_channel_template" name="Discuss Channel Template">
|
||||
<template id="mail.discuss_public_channel_template" name="Discuss Public Channel Template">
|
||||
<t t-call="mail.discuss_public_layout">
|
||||
<t t-set="head">
|
||||
<script>
|
||||
@@ -47,4 +47,19 @@
|
||||
</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();
|
||||
const channelId = <t t-out="channel_sudo.id"/>;
|
||||
createWelcomeView(channelId);
|
||||
});
|
||||
</script>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -94,11 +94,12 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
message = channel_channel_public_1.message_post(body='test', message_type='comment', author_id=self.users[2].partner_id.id, partner_ids=self.users[0].partner_id.ids)
|
||||
# add star
|
||||
message.toggle_message_starred()
|
||||
self.env.company.sudo().name = 'YourCompany'
|
||||
|
||||
self.maxDiff = None
|
||||
self.users[0].flush()
|
||||
self.users[0].invalidate_cache()
|
||||
with self.assertQueryCount(emp=50):
|
||||
with self.assertQueryCount(emp=90):
|
||||
init_messaging = self.users[0].with_user(self.users[0])._init_messaging()
|
||||
|
||||
self.assertEqual(init_messaging, {
|
||||
@@ -110,6 +111,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': user_root.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': 'General announcements for all employees.',
|
||||
'group_based_subscription': True,
|
||||
'id': channel_general.id,
|
||||
@@ -131,6 +133,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_public_1.id,
|
||||
@@ -152,6 +155,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_public_2.id,
|
||||
@@ -173,6 +177,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_group_1.id,
|
||||
@@ -194,6 +199,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_group_2.id,
|
||||
@@ -215,6 +221,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_private_1.id,
|
||||
@@ -236,6 +243,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'channel',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_channel_private_2.id,
|
||||
@@ -257,6 +265,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'group',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_group_1.id,
|
||||
@@ -316,6 +325,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'chat',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_chat_1.id,
|
||||
@@ -375,6 +385,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'chat',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_chat_2.id,
|
||||
@@ -434,6 +445,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'chat',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_chat_3.id,
|
||||
@@ -493,6 +505,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'chat',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_chat_4.id,
|
||||
@@ -552,6 +565,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'livechat',
|
||||
'create_uid': self.env.user.id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_livechat_1.id,
|
||||
@@ -617,6 +631,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'channel_type': 'livechat',
|
||||
'create_uid': self.env.ref('base.public_user').id,
|
||||
'custom_channel_name': False,
|
||||
'defaultDisplayMode': False,
|
||||
'description': False,
|
||||
'group_based_subscription': False,
|
||||
'id': channel_livechat_2.id,
|
||||
@@ -678,6 +693,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
'uuid': channel_livechat_2.uuid,
|
||||
},
|
||||
],
|
||||
'companyName': 'YourCompany',
|
||||
'mail_failures': [],
|
||||
'shortcodes': [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user