[IMP] mail: implement link preview with OpenGraph Protocol
OpenGraph Protocol https://ogp.me/ allow us to get some data from the link that are shared in message. task-2365881 closes odoo/odoo#82641 Related: odoo/enterprise#30867 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -57,6 +57,7 @@ class TestImLivechatMessage(TransactionCase):
|
||||
'is_discussion': False,
|
||||
'is_note': True,
|
||||
'is_notification': False,
|
||||
'linkPreviews': [],
|
||||
'message_type': 'notification',
|
||||
'messageReactionGroups': [],
|
||||
'model': 'mail.channel',
|
||||
|
||||
@@ -79,6 +79,7 @@ For more specific needs, you may also assign custom-defined actions
|
||||
'views/mail_ice_server_views.xml',
|
||||
'views/mail_channel_member_views.xml',
|
||||
'views/mail_channel_rtc_session_views.xml',
|
||||
'views/mail_link_preview_views.xml',
|
||||
'views/mail_channel_views.xml',
|
||||
'views/mail_shortcode_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
|
||||
@@ -577,3 +577,29 @@ class DiscussController(http.Controller):
|
||||
if guest_to_rename_sudo != guest and not request.env.user._is_admin():
|
||||
raise NotFound()
|
||||
guest_to_rename_sudo._update_name(name)
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Link preview API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/mail/link_preview', methods=['POST'], type='json', auth='public')
|
||||
def mail_link_preview(self, message_id):
|
||||
if not request.env['mail.link.preview'].sudo()._is_link_preview_enabled():
|
||||
return
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
message = guest.env['mail.message'].search([('id', '=', int(message_id))])
|
||||
if not message:
|
||||
return
|
||||
if not message.is_current_user_or_guest_author and not guest.env.user._is_admin():
|
||||
return
|
||||
guest.env['mail.link.preview'].sudo()._create_link_previews(message)
|
||||
|
||||
@http.route('/mail/link_preview/delete', methods=['POST'], type='json', auth='public')
|
||||
def mail_link_preview_delete(self, link_preview_id):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
link_preview_sudo = guest.env['mail.link.preview'].sudo().search([('id', '=', int(link_preview_id))])
|
||||
if not link_preview_sudo:
|
||||
return
|
||||
if not link_preview_sudo.message_id.is_current_user_or_guest_author and not guest.env.user._is_admin():
|
||||
return
|
||||
link_preview_sudo._delete_and_notify()
|
||||
|
||||
@@ -23,6 +23,7 @@ from . import mail_activity
|
||||
from . import mail_blacklist
|
||||
from . import mail_followers
|
||||
from . import mail_gateway_allowed
|
||||
from . import mail_link_preview
|
||||
from . import mail_message_reaction
|
||||
from . import mail_message_schedule
|
||||
from . import mail_message_subtype
|
||||
|
||||
@@ -113,6 +113,7 @@ class MailGuest(models.Model):
|
||||
'current_partner': False,
|
||||
'current_user_id': False,
|
||||
'current_user_settings': False,
|
||||
'hasLinkPreviewFeature': self.env['mail.link.preview']._is_link_preview_enabled(),
|
||||
'menu_id': False,
|
||||
'needaction_inbox_counter': False,
|
||||
'partner_root': {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from lxml import html, etree
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
from odoo import api, models, fields
|
||||
|
||||
|
||||
class LinkPreview(models.Model):
|
||||
_name = 'mail.link.preview'
|
||||
_description = "Store link preview data"
|
||||
|
||||
message_id = fields.Many2one('mail.message', string='Message', index=True, ondelete='cascade', required=True)
|
||||
source_url = fields.Char('url', required=True)
|
||||
og_type = fields.Char('type')
|
||||
og_title = fields.Char('title')
|
||||
og_image = fields.Char('image')
|
||||
og_description = fields.Text('description')
|
||||
og_mimetype = fields.Char('mimetype')
|
||||
image_mimetype = fields.Char('image_mimetype')
|
||||
create_date = fields.Datetime(index=True)
|
||||
|
||||
@api.model
|
||||
def _create_link_previews(self, message):
|
||||
tree = html.fromstring(message.body)
|
||||
urls = tree.xpath('//a/@href')
|
||||
link_previews = self.env['mail.link.preview']
|
||||
requests_session = requests.Session()
|
||||
# Some websites are blocking non browser user agent.
|
||||
requests_session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0'
|
||||
})
|
||||
for url in set(urls):
|
||||
if len(link_previews) >= 5:
|
||||
break
|
||||
link_previews |= self.env['mail.link.preview']._create_link_preview(url, message.id, requests_session)
|
||||
if not link_previews:
|
||||
return
|
||||
guest = self.env['mail.guest']._get_guest_from_context()
|
||||
if message.model == 'mail.channel' and message.res_id:
|
||||
target = self.env['mail.channel'].browse(message.res_id)
|
||||
elif self.env.user._is_public() and guest:
|
||||
target = guest
|
||||
else:
|
||||
target = self.env.user.partner_id
|
||||
self.env['bus.bus']._sendmany([(target, 'mail.link.preview/insert', link_previews._link_preview_format())])
|
||||
|
||||
@api.model
|
||||
def _create_link_preview(self, url, message_id, request_session):
|
||||
if self._is_domain_throttled(url):
|
||||
return self.env['mail.link.preview']
|
||||
link_preview_data = self._get_link_preview_from_url(url, request_session)
|
||||
if link_preview_data:
|
||||
link_preview_data['message_id'] = message_id
|
||||
return self.create(link_preview_data)
|
||||
return self.env['mail.link.preview']
|
||||
|
||||
def _delete_and_notify(self):
|
||||
notifications = []
|
||||
guest = self.env['mail.guest']._get_guest_from_context()
|
||||
for link_preview in self:
|
||||
if link_preview.message_id.model == 'mail.channel' and link_preview.message_id.res_id:
|
||||
target = self.env['mail.channel'].browse(link_preview.message_id.res_id)
|
||||
elif self.env.user._is_public() and guest:
|
||||
target = guest
|
||||
else:
|
||||
target = self.env.user.partner_id
|
||||
notifications.append((target, 'mail.link.preview/delete', {'id': link_preview.id}))
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
self.unlink()
|
||||
|
||||
@api.model
|
||||
def _is_link_preview_enabled(self):
|
||||
link_preview_throttle = int(self.env['ir.config_parameter'].sudo().get_param('mail.link_preview_throttle', 99))
|
||||
return link_preview_throttle > 0
|
||||
|
||||
@api.model
|
||||
def _is_domain_throttled(self, url):
|
||||
domain = urlparse(url).netloc
|
||||
date_interval = fields.Datetime.to_string((datetime.now() - relativedelta(seconds=10)))
|
||||
call_counter = self.search_count([
|
||||
('source_url', 'ilike', domain),
|
||||
('create_date', '>', date_interval),
|
||||
])
|
||||
link_preview_throttle = int(self.env['ir.config_parameter'].get_param('mail.link_preview_throttle', 99))
|
||||
return call_counter > link_preview_throttle
|
||||
|
||||
@api.model
|
||||
def _get_link_preview_from_url(self, url, request_session):
|
||||
try:
|
||||
response = request_session.head(url, timeout=3, allow_redirects=True)
|
||||
except requests.exceptions.RequestException:
|
||||
return False
|
||||
if response.status_code != requests.codes.ok:
|
||||
return False
|
||||
image_mimetype = (
|
||||
'image/bmp',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/tiff',
|
||||
'image/x-icon',
|
||||
)
|
||||
# Content-Type header can return a charset, but we just need the
|
||||
# mimetype (eg: image/jpeg;charset=ISO-8859-1)
|
||||
content_type = response.headers['Content-Type'].split(';')
|
||||
if response.headers['Content-Type'].startswith(image_mimetype):
|
||||
return {
|
||||
'image_mimetype': content_type[0],
|
||||
'source_url': url,
|
||||
}
|
||||
if response.headers['Content-Type'].startswith('text/html'):
|
||||
return self._get_link_preview_from_html(url, request_session)
|
||||
return False
|
||||
|
||||
def _get_link_preview_from_html(self, url, request_session):
|
||||
response = request_session.get(url, timeout=3)
|
||||
parser = etree.HTMLParser(encoding=response.encoding)
|
||||
tree = html.fromstring(response.content, parser=parser)
|
||||
og_title = tree.xpath('//meta[@property="og:title"]/@content')
|
||||
if not og_title:
|
||||
return False
|
||||
og_description = tree.xpath('//meta[@property="og:description"]/@content')
|
||||
og_type = tree.xpath('//meta[@property="og:type"]/@content')
|
||||
og_image = tree.xpath('//meta[@property="og:image"]/@content')
|
||||
og_mimetype = tree.xpath('//meta[@property="og:image:type"]/@content')
|
||||
return {
|
||||
'og_description': og_description[0] if og_description else None,
|
||||
'og_image': og_image[0] if og_image else None,
|
||||
'og_mimetype': og_mimetype[0] if og_mimetype else None,
|
||||
'og_title': og_title[0],
|
||||
'og_type': og_type[0] if og_type else None,
|
||||
'source_url': url,
|
||||
}
|
||||
|
||||
def _link_preview_format(self):
|
||||
return [{
|
||||
'id': preview.id,
|
||||
'message': {'id': preview.message_id.id},
|
||||
'image_mimetype': preview.image_mimetype,
|
||||
'og_description': preview.og_description,
|
||||
'og_image': preview.og_image,
|
||||
'og_mimetype': preview.og_mimetype,
|
||||
'og_title': preview.og_title,
|
||||
'og_type': preview.og_type,
|
||||
'source_url': preview.source_url,
|
||||
} for preview in self]
|
||||
@@ -104,6 +104,7 @@ class Message(models.Model):
|
||||
model = fields.Char('Related Document Model')
|
||||
res_id = fields.Many2oneReference('Related Document ID', model_field='model')
|
||||
record_name = fields.Char('Message Record Name') # name_get() of the related document
|
||||
link_preview_ids = fields.One2many('mail.link.preview', 'message_id', string='Link Previews', groups="base.group_erp_manager")
|
||||
# characteristics
|
||||
message_type = fields.Selection([
|
||||
('email', 'Email'),
|
||||
@@ -859,6 +860,7 @@ class Message(models.Model):
|
||||
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
|
||||
'attachment_ids': message_sudo.attachment_ids._attachment_format() if not legacy else message_sudo.attachment_ids._attachment_format(legacy=True),
|
||||
'trackingValues': allowed_tracking_ids._tracking_value_format(),
|
||||
'linkPreviews': message_sudo.link_preview_ids._link_preview_format(),
|
||||
'messageReactionGroups': reaction_groups,
|
||||
'record_name': record_name,
|
||||
})
|
||||
|
||||
@@ -189,6 +189,7 @@ class Users(models.Model):
|
||||
'current_partner': self.partner_id.mail_partner_format().get(self.partner_id),
|
||||
'current_user_id': self.id,
|
||||
'current_user_settings': self.env['res.users.settings']._find_or_create_for_user(self)._res_users_settings_format(),
|
||||
'hasLinkPreviewFeature': self.env['mail.link.preview']._is_link_preview_enabled(),
|
||||
'internalUserGroupId': self.env.ref('base.group_user').id,
|
||||
'menu_id': self.env['ir.model.data']._xmlid_to_res_id('mail.menu_root_discuss'),
|
||||
'needaction_inbox_counter': self.partner_id._get_needaction_count(),
|
||||
|
||||
@@ -65,3 +65,4 @@ access_res_users_settings_volumes_all,res.users.settings.volumes,model_res_users
|
||||
access_res_users_settings_volumes_user,res.users.settings.volumes,model_res_users_settings_volumes,base.group_user,1,1,1,1
|
||||
access_mail_template_reset,access.mail.template.reset,model_mail_template_reset,mail.group_mail_template_editor,1,1,1,1
|
||||
ir_actions_report_access_user,ir.actions.report.access.user,base.model_ir_actions_report,base.group_user,1,0,0,0
|
||||
access_mail_link_preview_admin,mail.link.preview.admin,model_mail_link_preview,base.group_erp_manager,1,1,1,1
|
||||
|
||||
|
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class LinkPreviewAsideView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {LinkPreviewAsideView}
|
||||
*/
|
||||
get linkPreviewAsideView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewAsideView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewAsideView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewAsideView);
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewAsideView" owl="1">
|
||||
<button class="o_LinkPreviewAside btn" t-attf-class="{{ className }}" t-on-click="linkPreviewAsideView.onClick" t-ref="root">
|
||||
<i class="o_LinkPreviewAside_itemUnlinkIcon fa fa-times"/>
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class LinkPreviewCardView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {LinkPreviewCardView}
|
||||
*/
|
||||
get linkPreviewCardView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewCardView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewCardView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewCardView);
|
||||
@@ -0,0 +1,18 @@
|
||||
.o_LinkPreviewCardView {
|
||||
max-width: $o-mail-link-preview-width;
|
||||
}
|
||||
.o_LinkPreviewCardView .row {
|
||||
min-height: $o-mail-link-preview-card-height;
|
||||
}
|
||||
|
||||
.o_LinkPreviewCardView_hasMultineLines {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
&.o_LinkPreviewCardView_description {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
&.o_LinkPreviewCardView_title {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewCardView" owl="1">
|
||||
<div class="o_LinkPreviewCardView card position-relative w-100 mb-2 rounded border border-light bg-white shadow-sm overflow-hidden" t-att-class="{ 'me-2': !linkPreviewCardView.linkPreviewListViewOwner.messageViewOwner.isInChatWindow }" t-attf-class="{{ className }}" t-on-mouseenter="linkPreviewCardView.onMouseEnter" t-on-mouseleave="linkPreviewCardView.onMouseLeave" t-ref="root">
|
||||
<div class="row g-0 flex-row-reverse h-100">
|
||||
<div class="col min-w-0" t-att-class="{ 'd-flex align-items-center': !linkPreviewCardView.linkPreview.og_description }">
|
||||
<div class="o_LinkPreviewCardView_content p-3 bg-white">
|
||||
<h6 class="o_LinkPreviewCardView_title card-title mb-0 me-2" t-attf-class="{{ linkPreviewCardView.linkPreview.og_description ? 'text-truncate' : 'o_LinkPreviewCardView_hasMultineLines overflow-hidden' }}">
|
||||
<a t-att-href="linkPreviewCardView.linkPreview.source_url" target="_blank" t-out="linkPreviewCardView.linkPreview.og_title"/>
|
||||
</h6>
|
||||
<p t-if="linkPreviewCardView.linkPreview.og_description" class="o_LinkPreviewCardView_description o_LinkPreviewCardView_hasMultineLines card-text mb-0 mt-2 text-muted small overflow-hidden" t-out="linkPreviewCardView.linkPreview.og_description"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 d-block border-end border-light">
|
||||
<a t-att-href="linkPreviewCardView.linkPreview.source_url" class="o_LinkPreviewCardView_imageLink" target="_blank">
|
||||
<img t-if="linkPreviewCardView.linkPreview.og_image" class="o_LinkPreviewCardView_image w-100 h-100 o_object_fit_cover" t-att-src="linkPreviewCardView.linkPreview.og_image" t-att-alt="linkPreviewCardView.linkPreview.og_title"/>
|
||||
<span t-else="" class="o_LinkPreviewCardView_imageEmpty d-flex align-items-center justify-content-center w-100 h-100 bg-100 text-300">
|
||||
<i class="fa fa-camera fa-2x"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="linkPreviewCardView.linkPreviewAsideView" class="o_LinkPreviewCardView_aside position-absolute top-0 end-0 small">
|
||||
<LinkPreviewAsideView className="'fa fa-stack p-0 opacity-75 opacity-100-hover'" record="linkPreviewCardView.linkPreviewAsideView"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class LinkPreviewDeleteConfirmView extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {DeleteMessageConfirmView}
|
||||
*/
|
||||
get linkPreviewDeleteConfirmView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewDeleteConfirmView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewDeleteConfirmView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewDeleteConfirmView);
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewDeleteConfirmView" owl="1">
|
||||
<div class="o_LinkPreviewDeleteConfirmView card bg-white" t-attf-class="{{ className }}" t-ref="root">
|
||||
<h4 class="m-3">Confirmation</h4>
|
||||
<hr class="mt-0 mb-3"/>
|
||||
<p class="o_LinkPreviewDeleteConfirmView_mainText mx-3 mb-3">Do you really want to delete this preview?</p>
|
||||
<hr class="mt-0 mb-3"/>
|
||||
<div class="o_LinkPreviewDeleteConfirmView_buttons mx-3 mb-3">
|
||||
<button class="o_LinkPreviewDeleteConfirmView_confirmButton btn btn-primary me-2" t-on-click="linkPreviewDeleteConfirmView.onClickOk">Ok</button>
|
||||
<button class="o_LinkPreviewDeleteConfirmView_cancelButton btn btn-secondary me-2" t-on-click="linkPreviewDeleteConfirmView.onClickCancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class LinkPreviewImageView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {LinkPreviewImageView}
|
||||
*/
|
||||
get linkPreviewImageView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewImageView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewImageView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewImageView);
|
||||
@@ -0,0 +1,4 @@
|
||||
.o_LinkPreviewImageView img {
|
||||
max-height: $o-mail-link-preview-height;
|
||||
max-width: $o-mail-link-preview-width;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewImageView" owl="1">
|
||||
<div class="o_LinkPreviewImageView position-relative mb-2" t-att-class="{ 'me-2': !linkPreviewImageView.linkPreviewListViewOwner.messageViewOwner.isInChatWindow }" t-attf-class="{{ className }}" t-on-mouseenter="linkPreviewImageView.onMouseEnter" t-on-mouseleave="linkPreviewImageView.onMouseLeave" t-ref="root">
|
||||
<a t-if="linkPreviewImageView.imageUrl" t-att-href="linkPreviewImageView.imageUrl" target="_blank">
|
||||
<img class="h-auto w-auto rounded" t-att-src="linkPreviewImageView.imageUrl"/>
|
||||
</a>
|
||||
<div t-if="linkPreviewImageView.linkPreviewAsideView" class="o_LinkPreviewCardView_aside position-absolute top-0 end-0 small">
|
||||
<LinkPreviewAsideView className="'btn btn-sm btn-dark mt-2 me-2 rounded opacity-75 opacity-100-hover'" record="linkPreviewImageView.linkPreviewAsideView"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class LinkPreviewListView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {LinkPreviewListView}
|
||||
*/
|
||||
get linkPreviewListView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewListView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewListView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewListView);
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewListView" owl="1">
|
||||
<div class="o_LinkPreviewListView d-flex flex-column mt-2" t-att-class="{ 'me-2 pe-4': linkPreviewListView.messageViewOwner.isInChatWindowAndIsAlignedLeft, 'ms-2 ps-4': linkPreviewListView.messageViewOwner.isInChatWindowAndIsAlignedRight }" t-attf-class="{{ className }}" t-ref="root">
|
||||
<div class="o_LinkPreviewListView_partialList o_LinkPreviewListView_partialListImages d-flex flex-grow-1 flex-wrap" t-att-class="{ 'justify-content-end': linkPreviewListView.messageViewOwner.isInChatWindowAndIsAlignedRight }">
|
||||
<LinkPreviewImageView t-foreach="linkPreviewListView.linkPreviewAsImageViews" t-as="linkPreviewImageView" t-key="linkPreviewImageView" record="linkPreviewImageView"/>
|
||||
</div>
|
||||
<div class="o_LinkPreviewListView_partialList o_LinkPreviewListView_partialListCards d-flex flex-grow-1 flex-wrap" t-att-class="{ 'justify-content-end': linkPreviewListView.messageViewOwner.isInChatWindowAndIsAlignedRight }">
|
||||
<LinkPreviewCardView t-foreach="linkPreviewListView.linkPreviewAsCardViews" t-as="linkPreviewCardView" t-key="linkPreviewCardView" record="linkPreviewCardView"/>
|
||||
<LinkPreviewVideoView t-foreach="linkPreviewListView.linkPreviewAsVideoViews" t-as="linkPreviewVideoView" t-key="linkPreviewVideoView" record="linkPreviewVideoView"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class LinkPreviewVideoView extends Component {
|
||||
|
||||
/**
|
||||
* @returns {LinkPreviewVideoView}
|
||||
*/
|
||||
get linkPreviewVideoView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(LinkPreviewVideoView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.LinkPreviewVideoView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(LinkPreviewVideoView);
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.o_LinkPreviewVideoView {
|
||||
max-width: $o-mail-link-preview-width;
|
||||
}
|
||||
.o_LinkPreviewVideoView .row {
|
||||
min-height: $o-mail-link-preview-card-height;
|
||||
}
|
||||
|
||||
.o_LinkPreviewVideoView_hasMultineLines {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
&.o_LinkPreviewVideoView_description {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
&.o_LinkPreviewVideoView_title {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.LinkPreviewVideoView" owl="1">
|
||||
<div class="o_LinkPreview o_LinkPreviewVideoView card position-relative w-100 mb-2 rounded border border-light bg-white shadow-sm overflow-hidden" t-att-class="{ 'me-2': !linkPreviewVideoView.linkPreviewListViewOwner.messageViewOwner.isInChatWindow }" t-attf-class="{{ className }}" t-on-mouseenter="linkPreviewVideoView.onMouseEnter" t-on-mouseleave="linkPreviewVideoView.onMouseLeave" t-ref="root">
|
||||
<div class="row g-0 flex-row-reverse h-100">
|
||||
<div class="col min-w-0" t-att-class="{ 'd-flex align-items-center': !linkPreviewVideoView.linkPreview.og_description }">
|
||||
<div class="o_LinkPreviewVideoView_content p-3 bg-white">
|
||||
<h6 class="o_LinkPreviewVideoView_title card-title mb-0 me-2" t-attf-class="{{ linkPreviewVideoView.linkPreview.og_description ? 'text-truncate' : 'o_LinkPreviewVideoView_hasMultineLines overflow-hidden' }}">
|
||||
<a t-att-href="linkPreviewVideoView.linkPreview.source_url" target="_blank" t-esc="linkPreviewVideoView.linkPreview.og_title"/>
|
||||
</h6>
|
||||
<p t-if="linkPreviewVideoView.linkPreview.og_description" class="o_LinkPreviewVideoView_description o_LinkPreviewVideoView_hasMultineLines card-text mb-0 mt-2 text-muted small overflow-hidden" t-out="linkPreviewVideoView.linkPreview.og_description"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4 d-block border-end border-light">
|
||||
<a t-att-href="linkPreviewVideoView.linkPreview.source_url" class="o_LinkPreviewVideoView_imageLink" target="_blank">
|
||||
<div t-if="linkPreviewVideoView.linkPreview.og_image" class="o_linkPreviewVideo_overlay position-relative h-100 opacity-trigger-hover">
|
||||
<img class="o_LinkPreviewVideoView_image img-fluid h-100 rounded-bottom o_object_fit_cover" t-att-src="linkPreviewVideoView.linkPreview.og_image" t-att-alt="linkPreviewVideoView.linkPreview.og_title"/>
|
||||
<div class="position-absolute top-50 start-50 translate-middle">
|
||||
<div class="btn btn-dark btn-lg rounded opacity-75 opacity-100-hover transition-base">
|
||||
<i class="fa fa-play text-white"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span t-else="" class="o_LinkPreviewVideoView_imageEmpty d-flex align-items-center justify-content-center w-100 h-100 bg-100 text-300">
|
||||
<i class="fa fa-camera fa-2x"/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="linkPreviewVideoView.linkPreviewAsideView" class="o_LinkPreviewVideoView_aside position-absolute top-0 end-0 small">
|
||||
<LinkPreviewAsideView className="'fa fa-stack p-0 opacity-75 opacity-100-hover'" record="linkPreviewVideoView.linkPreviewAsideView"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -206,6 +206,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<AttachmentList t-if="messageView.attachmentList" record="messageView.attachmentList" className="'position-relative'"/>
|
||||
<LinkPreviewListView t-if="messageView.linkPreviewListView" record="messageView.linkPreviewListView"/>
|
||||
<div t-if="messageView.message.messageReactionGroups.length > 0" class="position-relative d-flex flex-wrap"
|
||||
t-att-class="{ 'flex-row-reverse me-3': messageView.isInChatWindowAndIsAlignedRight, 'ms-3': !messageView.isInChatWindowAndIsAlignedRight and (messageView.message.isDiscussionOrNotification or messageView.message.message_type === 'sms')}" t-attf-class="{{ messageView.message.isDiscussionOrNotification or messageView.message.message_type === 'sms' ? 'mt-n2' : 'mt-1' }}">
|
||||
<t t-foreach="messageView.message.messageReactionGroups" t-as="messageReactionGroup" t-key="messageReactionGroup.localId">
|
||||
|
||||
@@ -586,6 +586,14 @@ registerModel({
|
||||
const message = messaging.models['Message'].insert(
|
||||
messaging.models['Message'].convertData(messageData)
|
||||
);
|
||||
if (this.messaging.hasLinkPreviewFeature) {
|
||||
this.messaging.rpc({
|
||||
route: `/mail/link_preview`,
|
||||
params: {
|
||||
message_id: message.id
|
||||
}
|
||||
}, { shadow: true });
|
||||
}
|
||||
for (const threadView of message.originThread.threadViews) {
|
||||
// Reset auto scroll to be able to see the newly posted message.
|
||||
threadView.update({ hasAutoScrollOnMessageReceived: true });
|
||||
@@ -841,7 +849,7 @@ registerModel({
|
||||
return Boolean(
|
||||
(this.hasThreadName && this.composer.thread) ||
|
||||
(this.hasFollowers && !this.composer.isLog) ||
|
||||
this.threadView && this.threadView.replyingToMessageView
|
||||
(this.threadView && this.threadView.replyingToMessageView)
|
||||
);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -88,6 +88,9 @@ registerModel({
|
||||
if (this.followerSubtypeList) {
|
||||
return 'FollowerSubtypeList';
|
||||
}
|
||||
if (this.linkPreviewDeleteConfirmView) {
|
||||
return 'LinkPreviewDeleteConfirmView';
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
@@ -104,7 +107,6 @@ registerModel({
|
||||
_computeFollowerSubtypeList() {
|
||||
return this.followerOwnerAsSubtypeList ? {} : clear();
|
||||
},
|
||||
/**
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -119,6 +121,13 @@ registerModel({
|
||||
}
|
||||
return true;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
*/
|
||||
_computeLinkPreviewDeleteConfirmView() {
|
||||
return this.linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm ? {} : clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
@@ -143,6 +152,9 @@ registerModel({
|
||||
if (this.deleteMessageConfirmView) {
|
||||
return this.deleteMessageConfirmView;
|
||||
}
|
||||
if (this.linkPreviewDeleteConfirmView) {
|
||||
return this.linkPreviewDeleteConfirmView;
|
||||
}
|
||||
if (this.followerSubtypeList) {
|
||||
return this.followerSubtypeList;
|
||||
}
|
||||
@@ -218,6 +230,15 @@ registerModel({
|
||||
inverse: 'dialogOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm: one('LinkPreviewAsideView', {
|
||||
inverse: 'linkPreviewDeleteConfirmDialog',
|
||||
readonly: true,
|
||||
}),
|
||||
linkPreviewDeleteConfirmView: one('LinkPreviewDeleteConfirmView', {
|
||||
compute: '_computeLinkPreviewDeleteConfirmView',
|
||||
inverse: 'dialogOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
followerOwnerAsSubtypeList: one('Follower', {
|
||||
identifying: true,
|
||||
inverse: 'followerSubtypeListDialog',
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, many, one } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreview',
|
||||
recordMethods: {
|
||||
async remove() {
|
||||
await this.messaging.rpc({
|
||||
route: '/mail/link_preview/delete',
|
||||
params: { link_preview_id: this.id },
|
||||
}, { shadow: true });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsCard() {
|
||||
return !this.isVideo && !this.isImage;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsDeletable() {
|
||||
if (!this.message) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
(this.message.author && this.message.author === this.messaging.currentPartner) ||
|
||||
(this.message.guestAuthor && this.message.guestAuthor === this.messaging.currentGuest)
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsImage() {
|
||||
return Boolean(this.image_mimetype || this.og_mimetype === 'image/gif');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean|FieldCommand}
|
||||
*/
|
||||
_computeIsVideo() {
|
||||
if (!this.og_type) {
|
||||
return clear();
|
||||
}
|
||||
return this.og_type.startsWith('video') && !this.isImage;
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
id: attr({
|
||||
identifying: true,
|
||||
}),
|
||||
image_mimetype: attr(),
|
||||
isCard: attr({
|
||||
compute: '_computeIsCard',
|
||||
}),
|
||||
isDeletable: attr({
|
||||
compute: '_computeIsDeletable',
|
||||
}),
|
||||
isImage: attr({
|
||||
compute: '_computeIsImage',
|
||||
}),
|
||||
isVideo: attr({
|
||||
compute: '_computeIsVideo',
|
||||
}),
|
||||
linkPreviewCardView: many('LinkPreviewCardView', {
|
||||
inverse: 'linkPreview',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewImageView: many('LinkPreviewImageView', {
|
||||
inverse: 'linkPreview',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewVideoView: many('LinkPreviewVideoView', {
|
||||
inverse: 'linkPreview',
|
||||
isCausal: true,
|
||||
}),
|
||||
message: one('Message', {
|
||||
inverse: 'linkPreviews',
|
||||
}),
|
||||
og_description: attr(),
|
||||
og_image: attr(),
|
||||
og_mimetype: attr(),
|
||||
og_title: attr(),
|
||||
og_type: attr(),
|
||||
source_url: attr(),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { one } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewAsideView',
|
||||
identifyingMode: 'xor',
|
||||
recordMethods: {
|
||||
/**
|
||||
* Handles the click on delete link preview and open the confirm dialog.
|
||||
*/
|
||||
onClick() {
|
||||
this.update({ linkPreviewDeleteConfirmDialog: {} });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
*/
|
||||
_computeLinkPreview() {
|
||||
if (this.linkPreviewCardView) {
|
||||
return this.linkPreviewCardView.linkPreview;
|
||||
}
|
||||
if (this.linkPreviewImageView) {
|
||||
return this.linkPreviewImageView.linkPreview;
|
||||
}
|
||||
if (this.linkPreviewVideoView) {
|
||||
return this.linkPreviewVideoView.linkPreview;
|
||||
}
|
||||
return clear();
|
||||
}
|
||||
},
|
||||
fields: {
|
||||
linkPreview: one('LinkPreview', {
|
||||
compute: '_computeLinkPreview',
|
||||
required: true,
|
||||
}),
|
||||
linkPreviewCardView: one('LinkPreviewCardView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsideView',
|
||||
}),
|
||||
linkPreviewDeleteConfirmDialog: one('Dialog', {
|
||||
inverse: 'linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewImageView: one('LinkPreviewImageView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsideView',
|
||||
}),
|
||||
linkPreviewVideoView: one('LinkPreviewVideoView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsideView',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, one } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewCardView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* Handles mouse enter event for the container of this element.
|
||||
*/
|
||||
onMouseEnter() {
|
||||
this.update({ isHovered: true });
|
||||
},
|
||||
/**
|
||||
* Handles mouse leave event for the container of this element.
|
||||
*/
|
||||
onMouseLeave() {
|
||||
this.update({ isHovered: false });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewAsideView|FieldCommand}
|
||||
*/
|
||||
_computeLinkPreviewAsideView() {
|
||||
if (!this.linkPreview.isDeletable) {
|
||||
return clear();
|
||||
}
|
||||
if (this.messaging.device.isMobileDevice) {
|
||||
return {};
|
||||
}
|
||||
if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) {
|
||||
return {};
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
isHovered: attr({
|
||||
default: false,
|
||||
}),
|
||||
linkPreview: one('LinkPreview', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewCardView',
|
||||
}),
|
||||
linkPreviewAsideView: one('LinkPreviewAsideView', {
|
||||
compute: '_computeLinkPreviewAsideView',
|
||||
inverse: 'linkPreviewCardView',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewListViewOwner: one('LinkPreviewListView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsCardViews',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, one } from '@mail/model/model_field';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewDeleteConfirmView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* Returns whether the given html element is inside this attachment delete confirm view.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsElement(element) {
|
||||
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
|
||||
},
|
||||
onClickCancel() {
|
||||
this.dialogOwner.delete();
|
||||
},
|
||||
onClickOk() {
|
||||
this.linkPreview.remove();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreview}
|
||||
*/
|
||||
_computeLinkPreview() {
|
||||
return this.dialogOwner.linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm.linkPreview;
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
component: attr(),
|
||||
dialogOwner: one('Dialog', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewDeleteConfirmView',
|
||||
}),
|
||||
linkPreview: one('LinkPreview', {
|
||||
compute: '_computeLinkPreview',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, one } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewImageView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* Handles mouse enter event for the container of this element.
|
||||
*/
|
||||
onMouseEnter() {
|
||||
this.update({ isHovered: true });
|
||||
},
|
||||
/**
|
||||
* Handles mouse leave event for the container of this element.
|
||||
*/
|
||||
onMouseLeave() {
|
||||
this.update({ isHovered: false });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewAsideView|FieldCommand}
|
||||
*/
|
||||
_computeLinkPreviewAsideView() {
|
||||
if (!this.linkPreview.isDeletable) {
|
||||
return clear();
|
||||
}
|
||||
if (this.messaging.device.isMobileDevice) {
|
||||
return {};
|
||||
}
|
||||
if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) {
|
||||
return {};
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_computeImageUrl() {
|
||||
return this.linkPreview.og_image ? this.linkPreview.og_image : this.linkPreview.source_url;
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
imageUrl: attr({
|
||||
compute: '_computeImageUrl',
|
||||
}),
|
||||
isHovered: attr({
|
||||
default: false,
|
||||
}),
|
||||
linkPreview: one('LinkPreview', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewImageView',
|
||||
}),
|
||||
linkPreviewAsideView: one('LinkPreviewAsideView', {
|
||||
compute: '_computeLinkPreviewAsideView',
|
||||
inverse: 'linkPreviewImageView',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewListViewOwner: one('LinkPreviewListView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsImageViews',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { many, one } from '@mail/model/model_field';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewListView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewCardView[]}
|
||||
*/
|
||||
_computeLinkPreviewAsCardViews() {
|
||||
return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isCard).map(linkPreview => ({ linkPreview }));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewImageView[]}
|
||||
*/
|
||||
_computeLinkPreviewAsImageViews() {
|
||||
return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isImage).map(linkPreview => ({ linkPreview }));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewVideoView[]}
|
||||
*/
|
||||
_computeLinkPreviewAsVideoViews() {
|
||||
return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isVideo).map(linkPreview => ({ linkPreview }));
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
/**
|
||||
* Determines if we are in the ChatWindow view AND if the message is left aligned
|
||||
*/
|
||||
linkPreviewAsCardViews: many('LinkPreviewCardView', {
|
||||
compute: '_computeLinkPreviewAsCardViews',
|
||||
inverse: 'linkPreviewListViewOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewAsImageViews: many('LinkPreviewImageView', {
|
||||
compute: '_computeLinkPreviewAsImageViews',
|
||||
inverse: 'linkPreviewListViewOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewAsVideoViews: many('LinkPreviewVideoView', {
|
||||
compute: '_computeLinkPreviewAsVideoViews',
|
||||
inverse: 'linkPreviewListViewOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
messageViewOwner: one('MessageView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewListView',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { one, attr } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'LinkPreviewVideoView',
|
||||
recordMethods: {
|
||||
/**
|
||||
* Handles mouse enter event for the container of this element.
|
||||
*/
|
||||
onMouseEnter() {
|
||||
this.update({ isHovered: true });
|
||||
},
|
||||
/**
|
||||
* Handles mouse leave event for the container of this element.
|
||||
*/
|
||||
onMouseLeave() {
|
||||
this.update({ isHovered: false });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {LinkPreviewAsideView|FieldCommand}
|
||||
*/
|
||||
_computeLinkPreviewAsideView() {
|
||||
if (!this.linkPreview.isDeletable) {
|
||||
return clear();
|
||||
}
|
||||
if (this.messaging.device.isMobileDevice) {
|
||||
return {};
|
||||
}
|
||||
if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) {
|
||||
return {};
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
isHovered: attr({
|
||||
default: false,
|
||||
}),
|
||||
linkPreview: one('LinkPreview', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewVideoView',
|
||||
}),
|
||||
linkPreviewAsideView: one('LinkPreviewAsideView', {
|
||||
compute: '_computeLinkPreviewAsideView',
|
||||
inverse: 'linkPreviewVideoView',
|
||||
isCausal: true,
|
||||
}),
|
||||
linkPreviewListViewOwner: one('LinkPreviewListView', {
|
||||
identifying: true,
|
||||
inverse: 'linkPreviewAsVideoViews',
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -51,6 +51,7 @@ registerModel({
|
||||
if ('is_notification' in data) {
|
||||
data2.is_notification = data.is_notification;
|
||||
}
|
||||
data2.linkPreviews = data.linkPreviews;
|
||||
if ('messageReactionGroups' in data) {
|
||||
data2.messageReactionGroups = data.messageReactionGroups;
|
||||
}
|
||||
@@ -795,6 +796,9 @@ registerModel({
|
||||
lastTrackingValue: one('TrackingValue', {
|
||||
compute: '_computeLastTrackingValue',
|
||||
}),
|
||||
linkPreviews: many('LinkPreview', {
|
||||
inverse: 'message',
|
||||
}),
|
||||
/**
|
||||
* Groups of reactions per content allowing to know the number of
|
||||
* reactions for each.
|
||||
|
||||
@@ -386,6 +386,9 @@ registerModel({
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
_computeLinkPreviewListView() {
|
||||
return (this.message && this.message.linkPreviews.length > 0) ? {} : clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
@@ -611,6 +614,11 @@ registerModel({
|
||||
compute: '_computeIsSquashed',
|
||||
default: false,
|
||||
}),
|
||||
linkPreviewListView: one('LinkPreviewListView', {
|
||||
compute: '_computeLinkPreviewListView',
|
||||
inverse: 'messageViewOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* Determines the message action list of this message view (if any).
|
||||
*/
|
||||
|
||||
@@ -394,6 +394,7 @@ registerModel({
|
||||
fetchImStatusTimerDuration: attr({
|
||||
default: 50 * 1000,
|
||||
}),
|
||||
hasLinkPreviewFeature: attr(),
|
||||
history: one('Mailbox', {
|
||||
default: {},
|
||||
inverse: 'messagingAsHistory',
|
||||
|
||||
@@ -46,6 +46,8 @@ registerModel({
|
||||
* @param {Object} param0.current_partner
|
||||
* @param {integer} param0.current_user_id
|
||||
* @param {Object} param0.current_user_settings
|
||||
* @param {boolean} [param0.hasLinkPreviewFeature]
|
||||
* @param {integer} [param0.internalUserGroupId]
|
||||
* @param {integer} [param0.needaction_inbox_counter=0]
|
||||
* @param {Object} param0.partner_root
|
||||
* @param {Object[]} [param0.shortcodes=[]]
|
||||
@@ -59,12 +61,13 @@ registerModel({
|
||||
currentGuest,
|
||||
current_user_id,
|
||||
current_user_settings,
|
||||
hasLinkPreviewFeature,
|
||||
internalUserGroupId,
|
||||
menu_id,
|
||||
needaction_inbox_counter = 0,
|
||||
partner_root,
|
||||
shortcodes = [],
|
||||
starred_counter = 0
|
||||
starred_counter = 0,
|
||||
}) {
|
||||
const discuss = this.messaging.discuss;
|
||||
// partners first because the rest of the code relies on them
|
||||
@@ -99,9 +102,7 @@ registerModel({
|
||||
}
|
||||
discuss.update({ menu_id });
|
||||
// company related data
|
||||
this.messaging.update({ companyName });
|
||||
|
||||
this.messaging.update({ internalUserGroupId });
|
||||
this.messaging.update({ companyName, hasLinkPreviewFeature, internalUserGroupId });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -69,6 +69,16 @@ registerModel({
|
||||
return;
|
||||
}
|
||||
return this._handleNotificationChannelMessage(message.payload);
|
||||
case 'mail.link.preview/insert':
|
||||
this.messaging.models['LinkPreview'].insert(message.payload);
|
||||
return;
|
||||
case 'mail.link.preview/delete': {
|
||||
const linkPreview = this.messaging.models['LinkPreview'].findFromIdentifyingData(message.payload);
|
||||
if (linkPreview) {
|
||||
linkPreview.delete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'mail.message/delete':
|
||||
return this._handleNotificationMessageDelete(message.payload);
|
||||
case 'mail.message/inbox':
|
||||
|
||||
@@ -30,3 +30,7 @@ $o-mail-discuss-sidebar-scrollbar-width: 15px !default;
|
||||
|
||||
$o-mail-message-sidebar-width: 48px !default;
|
||||
$o-mail-composer-text-input-height: 40px !default;
|
||||
|
||||
$o-mail-link-preview-width: 320px !default;
|
||||
$o-mail-link-preview-height: 240px !default;
|
||||
$o-mail-link-preview-card-height: 80px !default;
|
||||
|
||||
@@ -87,6 +87,9 @@ patch(MockServer.prototype, 'mail', {
|
||||
if (route === '/mail/init_messaging') {
|
||||
return this._mockRouteMailInitMessaging();
|
||||
}
|
||||
if (route === '/mail/link_preview') {
|
||||
return this._mockRouteMailLinkPreview(args.message_id);
|
||||
}
|
||||
if (route === '/mail/load_message_failures') {
|
||||
return this._mockRouteMailLoadMessageFailures();
|
||||
}
|
||||
@@ -423,6 +426,32 @@ patch(MockServer.prototype, 'mail', {
|
||||
_mockRouteMailInitMessaging() {
|
||||
return this._mockResUsers_InitMessaging([this.currentUserId]);
|
||||
},
|
||||
/**
|
||||
* Simulates `/mail/link_preview` route.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} message_id
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockRouteMailLinkPreview(message_id) {
|
||||
const linkPreviews = [];
|
||||
const [message] = this.pyEnv['mail.message'].searchRead([['id', '=', message_id]]);
|
||||
if (message.body === 'https://make-link-preview.com') {
|
||||
const linkPreviewId = this.pyEnv['mail.link.preview'].create({
|
||||
og_description: "test description",
|
||||
og_title: 'Article title',
|
||||
og_type: 'article',
|
||||
source_url: 'https://make-link-preview.com',
|
||||
});
|
||||
const [linkPreview] = this.pyEnv['mail.link.preview'].searchRead([['id', '=', linkPreviewId]]);
|
||||
linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview));
|
||||
let target = this.currentPartner;
|
||||
if (message.model === 'mail.channel') {
|
||||
target = this.pyEnv['mail.channel'].search([['id', '=', message.res_id]]);
|
||||
}
|
||||
this.pyEnv['bus.bus']._sendmany([[target, 'mail.link.preview/insert', linkPreviews]]);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Simulates the `/mail/load_message_failures` route.
|
||||
*
|
||||
@@ -1368,6 +1397,24 @@ patch(MockServer.prototype, 'mail', {
|
||||
seen_message_id: message_id,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Simulates the `_link_preview_format` method of `mail.link.preview`.
|
||||
*
|
||||
* @private
|
||||
* @param {object} linkPreview
|
||||
*/
|
||||
_mockMailLinkPreviewFormat(linkPreview) {
|
||||
return {
|
||||
id: linkPreview.id,
|
||||
image_mimetype: linkPreview.image_mimetype,
|
||||
og_description: linkPreview.og_description,
|
||||
og_image: linkPreview.og_image,
|
||||
og_mimetype: linkPreview.og_mimetype,
|
||||
og_title: linkPreview.og_title,
|
||||
og_type: linkPreview.og_type,
|
||||
source_url: linkPreview.source_url
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Simulates `mark_all_as_read` on `mail.message`.
|
||||
*
|
||||
@@ -1496,10 +1543,16 @@ patch(MockServer.prototype, 'mail', {
|
||||
'res.partner',
|
||||
[['id', 'in', message.partner_ids]],
|
||||
);
|
||||
const linkPreviews = this.getRecords('mail.link.preview', [
|
||||
['id', 'in', message.link_preview_ids],
|
||||
]);
|
||||
const linkPreviewsFormatted = linkPreviews.map(linkPreview => this._mockMailLinkPreviewFormat(linkPreview));
|
||||
|
||||
const response = Object.assign({}, message, {
|
||||
attachment_ids: formattedAttachments,
|
||||
author: formattedAuthor,
|
||||
history_partner_ids: historyPartnerIds,
|
||||
linkPreviews: linkPreviewsFormatted,
|
||||
needaction_partner_ids: needactionPartnerIds,
|
||||
notifications,
|
||||
parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false,
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
|
||||
addModelNamesToFetch([
|
||||
'mail.activity', 'mail.activity.type', 'mail.channel', 'mail.channel.member',
|
||||
'mail.followers', 'mail.message', 'mail.message.subtype', 'mail.notification',
|
||||
'mail.shortcode', 'mail.template', 'mail.tracking.value', 'res.users.settings',
|
||||
'res.users.settings.volumes'
|
||||
'mail.followers', 'mail.link.preview', 'mail.message', 'mail.message.subtype',
|
||||
'mail.notification', 'mail.shortcode', 'mail.template', 'mail.tracking.value',
|
||||
'res.users.settings', 'res.users.settings.volumes'
|
||||
]);
|
||||
|
||||
addFakeModel('res.fake', {
|
||||
|
||||
@@ -107,6 +107,14 @@ function getClick({ afterNextRender }) {
|
||||
};
|
||||
}
|
||||
|
||||
function getMouseenter({ afterNextRender }) {
|
||||
return async function mouseenter(selector) {
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(selector).dispatchEvent(new window.MouseEvent('mouseenter'))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function getOpenDiscuss(afterEvent, webClient, { context = {}, params, ...props } = {}) {
|
||||
return async function openDiscuss({ waitUntilMessagesLoaded = true } = {}) {
|
||||
const actionOpenDiscuss = {
|
||||
@@ -280,6 +288,7 @@ async function start(param0 = {}) {
|
||||
env: webClient.env,
|
||||
insertText,
|
||||
messaging: webClient.env.services.messaging.modelManager.messaging,
|
||||
mouseenter: getMouseenter({ afterNextRender }),
|
||||
openDiscuss: getOpenDiscuss(afterEvent, webClient, discuss),
|
||||
openView,
|
||||
openFormView: getOpenFormView(afterEvent, openView),
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
QUnit.module('link_preview_tests.js');
|
||||
|
||||
const linkPreviewGifPayload = {
|
||||
og_description: 'test description',
|
||||
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
|
||||
og_mimetype: 'image/gif',
|
||||
og_title: 'Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs',
|
||||
og_type: 'video.other',
|
||||
source_url: 'https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023',
|
||||
};
|
||||
const linkPreviewCardPayload = {
|
||||
og_description: 'Description',
|
||||
og_title: 'Article title',
|
||||
og_type: 'article',
|
||||
source_url: 'https://www.odoo.com',
|
||||
};
|
||||
const linkPreviewCardImagePayload = {
|
||||
og_description: 'Description',
|
||||
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
|
||||
og_title: 'Article title',
|
||||
og_type: 'article',
|
||||
source_url: 'https://www.odoo.com',
|
||||
};
|
||||
const linkPreviewVideoPayload = {
|
||||
og_description: 'Description',
|
||||
og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif',
|
||||
og_title: 'video title',
|
||||
og_type: 'video.other',
|
||||
source_url: 'https://www.odoo.com',
|
||||
};
|
||||
const linkPreviewImagePayload = {
|
||||
image_mimetype: 'image/jpg',
|
||||
source_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg',
|
||||
};
|
||||
|
||||
QUnit.test('auto layout with link preview list', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message .o_LinkPreviewListView',
|
||||
"Should have a link preview list in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('auto layout with link preview as gif', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewImageView',
|
||||
"Should have a link preview gif in the DOM"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('simplest card layout', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView',
|
||||
"should have link preview in DOM"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView_title',
|
||||
"Should display the link preview title"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView_description',
|
||||
"Link preview should show the link description"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('simplest card layout with image', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardImagePayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView',
|
||||
"should have link preview in DOM"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView_title',
|
||||
"Should display the link preview title"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView_description',
|
||||
"Link preview should show the link description"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewCardView_image',
|
||||
"Should display an image inside the link preview card"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Link preview video layout', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewVideoView',
|
||||
"should have link preview video in DOM"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewVideoView_title',
|
||||
"Should display the link preview title"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewVideoView_description',
|
||||
"Link preview should show the link description"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_linkPreviewVideo_overlay',
|
||||
"Should display overlay inside the link preview video image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Link preview image layout', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewImageView',
|
||||
"should have link preview image"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Remove link preview Gif', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss, click, mouseenter } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
await mouseenter('.o_LinkPreviewImageView');
|
||||
await click('.o_LinkPreviewAside');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewDeleteConfirmView',
|
||||
'Should have a link preview confirmation dialog'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Remove link preview card', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss, click, mouseenter } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
await mouseenter('.o_LinkPreviewCardView');
|
||||
await click('.o_LinkPreviewAside');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewDeleteConfirmView',
|
||||
'Should have a link preview confirmation dialog'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Remove link preview video', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss, click, mouseenter } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
await mouseenter('.o_LinkPreviewVideoView');
|
||||
await click('.o_LinkPreviewAside');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewDeleteConfirmView',
|
||||
'Should have a link preview confirmation dialog'
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Remove link preview image', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload);
|
||||
const mailChannelId = pyEnv['mail.channel'].create({});
|
||||
pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
link_preview_ids: [linkPreviewId],
|
||||
message_type: 'comment',
|
||||
model: "mail.channel",
|
||||
res_id: mailChannelId,
|
||||
});
|
||||
const { openDiscuss, click, mouseenter } = await start({
|
||||
discuss: {
|
||||
params: {
|
||||
default_active_id: `mail.channel_${mailChannelId}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
await openDiscuss();
|
||||
|
||||
await mouseenter('.o_LinkPreviewImageView');
|
||||
await click('.o_LinkPreviewAside');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_LinkPreviewDeleteConfirmView',
|
||||
'Should have a link preview confirmation dialog'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_get_model_definitions
|
||||
from . import test_link_preview
|
||||
from . import test_mail_channel
|
||||
from . import test_mail_channel_as_guest
|
||||
from . import test_mail_channel_member
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from functools import partial
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from unittest.mock import patch
|
||||
import requests
|
||||
|
||||
mail_channel_new_test_user = partial(mail_new_test_user, context={'mail_channel_nosubscribe': False})
|
||||
|
||||
|
||||
def _patched_get_html(*args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response._content = b"""
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="Test title">
|
||||
<meta property="og:description" content="Test description">
|
||||
</head>
|
||||
</html>
|
||||
"""
|
||||
response.headers["Content-Type"] = 'text/html'
|
||||
return response
|
||||
|
||||
def _patch_head_html(*args, **kwargs):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response.headers["Content-Type"] = 'text/html'
|
||||
return response
|
||||
|
||||
|
||||
class TestLinkPreview(MailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_1 = mail_channel_new_test_user(
|
||||
cls.env, login='user_1',
|
||||
name='User 1',
|
||||
groups='base.group_user')
|
||||
|
||||
cls.public_channel = cls.env['mail.channel'].create({
|
||||
'name': 'Public channel of user 1',
|
||||
'public': 'public',
|
||||
'channel_type': 'channel',
|
||||
})
|
||||
cls.public_channel.channel_member_ids.unlink()
|
||||
|
||||
def test_01_link_preview_throttle(self):
|
||||
with patch.object(requests.Session, 'get', _patched_get_html), patch.object(requests.Session, 'head', _patch_head_html):
|
||||
throttle = int(self.env['ir.config_parameter'].sudo().get_param('mail.link_preview_throttle', 99))
|
||||
link_previews = []
|
||||
for _ in range(throttle):
|
||||
link_previews.append({'source_url': 'https://thisdomainedoentexist.nothing', 'message_id': 1})
|
||||
self.env['mail.link.preview'].create(link_previews)
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': self.public_channel.id,
|
||||
'body': '<a href="https://thisdomainedoentexist.nothing">Nothing link</a>',
|
||||
})
|
||||
self.env['mail.link.preview']._create_link_previews(message)
|
||||
link_preview_count = self.env['mail.link.preview'].search_count([('source_url', '=', 'https://thisdomainedoentexist.nothing')])
|
||||
self.assertEqual(link_preview_count, throttle + 1)
|
||||
|
||||
def test_02_link_preview_create(self):
|
||||
with patch.object(requests.Session, 'get', _patched_get_html), patch.object(requests.Session, 'head', _patch_head_html):
|
||||
message = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
'res_id': self.public_channel.id,
|
||||
'body': '<a href="https://thisdomainedoentexist.nothing">Nothing link</a>',
|
||||
})
|
||||
self.env['mail.link.preview']._create_link_previews(message)
|
||||
self.assertBusNotifications(
|
||||
[(self.cr.dbname, 'mail.channel', self.public_channel.id)],
|
||||
message_items=[{
|
||||
'type': 'mail.link.preview/insert',
|
||||
'payload': [{
|
||||
'id': link_preview.id,
|
||||
'message': {'id': message.id},
|
||||
'image_mimetype': False,
|
||||
'og_description': 'Test description',
|
||||
'og_image': False,
|
||||
'og_mimetype': False,
|
||||
'og_title': 'Test title',
|
||||
'og_type': False,
|
||||
'source_url': 'https://thisdomainedoentexist.nothing',
|
||||
} for link_preview in message.link_preview_ids]
|
||||
}]
|
||||
)
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="model">mail.guest</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Guest">
|
||||
<tree string="Guests">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="country_id"/>
|
||||
@@ -17,7 +17,7 @@
|
||||
</record>
|
||||
|
||||
<record id="mail_guest_action" model="ir.actions.act_window">
|
||||
<field name="name">User Settings</field>
|
||||
<field name="name">Guests</field>
|
||||
<field name="res_model">mail.guest</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<record id="mail_link_preview_view_tree" model="ir.ui.view">
|
||||
<field name="name">mail.link.preview.tree</field>
|
||||
<field name="model">mail.link.preview</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Link Previews">
|
||||
<field name="id"/>
|
||||
<field name="source_url"/>
|
||||
<field name="og_title"/>
|
||||
<field name="og_type"/>
|
||||
<field name="image_mimetype"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mail_link_preview_action" model="ir.actions.act_window">
|
||||
<field name="name">Link Previews</field>
|
||||
<field name="res_model">mail.link.preview</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -8,9 +8,9 @@
|
||||
sequence="5"
|
||||
/>
|
||||
|
||||
<record id="base.menu_email" model="ir.ui.menu">
|
||||
<record id="base.menu_email" model="ir.ui.menu">
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
</record>
|
||||
|
||||
<!-- Under Technical/Email -->
|
||||
<menuitem name="Emails"
|
||||
@@ -134,6 +134,11 @@
|
||||
action="mail_message_reaction_action"
|
||||
parent="mail.mail_menu_technical"
|
||||
sequence="54"/>
|
||||
<menuitem id="mail_link_preview_menu"
|
||||
name="Link Previews"
|
||||
action="mail_link_preview_action"
|
||||
parent="mail.mail_menu_technical"
|
||||
sequence="55"/>
|
||||
|
||||
<!--
|
||||
This menuitem will be activated by integrations modules (like github, twitter, ...). It
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="model">mail.message.reaction</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Reaction">
|
||||
<tree string="Reactions">
|
||||
<field name="id"/>
|
||||
<field name="message_id"/>
|
||||
<field name="content"/>
|
||||
|
||||
@@ -113,6 +113,7 @@ class TestDiscussFullPerformance(TransactionCase):
|
||||
The point of having a separate getter is to allow it to be overriden.
|
||||
"""
|
||||
return {
|
||||
'hasLinkPreviewFeature': True,
|
||||
'needaction_inbox_counter': 1,
|
||||
'starred_counter': 1,
|
||||
'channels': [
|
||||
|
||||
@@ -1094,7 +1094,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
|
||||
with self.assertQueryCount(employee=18):
|
||||
with self.assertQueryCount(employee=19):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 2)
|
||||
for message in res:
|
||||
@@ -1122,7 +1122,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
|
||||
with self.assertQueryCount(employee=13):
|
||||
with self.assertQueryCount(employee=14):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 6)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user