[IMP] mail, web: add message reactions with emojis
task-2361194 closes odoo/odoo#75630 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
bc8f4373c3
commit
63ff23c218
@@ -209,6 +209,62 @@ class DiscussController(http.Controller):
|
||||
raise NotFound()
|
||||
return attachment_sudo.unlink()
|
||||
|
||||
@http.route('/mail/message/add_reaction', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_add_reaction(self, message_id, content):
|
||||
guest_sudo = request.env['mail.guest']._get_guest_from_request(request).sudo()
|
||||
message_sudo = guest_sudo.env['mail.message'].browse(int(message_id)).exists()
|
||||
if not message_sudo:
|
||||
raise NotFound()
|
||||
if request.env.user.sudo()._is_public():
|
||||
if not guest_sudo or not message_sudo.model == 'mail.channel' or message_sudo.res_id not in guest_sudo.channel_ids.ids:
|
||||
raise NotFound()
|
||||
message_sudo._message_add_reaction(content=content)
|
||||
guests = [('insert', {'id': guest_sudo.id})]
|
||||
partners = []
|
||||
else:
|
||||
message_sudo.sudo(False)._message_add_reaction(content=content)
|
||||
guests = []
|
||||
partners = [('insert', {'id': request.env.user.partner_id.id})]
|
||||
reactions = message_sudo.env['mail.message.reaction'].search([('message_id', '=', message_sudo.id), ('content', '=', content)])
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'messageId': message_sudo.id,
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'partners': partners,
|
||||
})],
|
||||
}
|
||||
|
||||
@http.route('/mail/message/remove_reaction', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_remove_reaction(self, message_id, content):
|
||||
guest_sudo = request.env['mail.guest']._get_guest_from_request(request).sudo()
|
||||
message_sudo = guest_sudo.env['mail.message'].browse(int(message_id)).exists()
|
||||
if not message_sudo:
|
||||
raise NotFound()
|
||||
if request.env.user.sudo()._is_public():
|
||||
if not guest_sudo or not message_sudo.model == 'mail.channel' or message_sudo.res_id not in guest_sudo.channel_ids.ids:
|
||||
raise NotFound()
|
||||
message_sudo._message_remove_reaction(content=content)
|
||||
guests = [('insert-and-unlink', {'id': guest_sudo.id})]
|
||||
partners = []
|
||||
else:
|
||||
message_sudo.sudo(False)._message_remove_reaction(content=content)
|
||||
guests = []
|
||||
partners = [('insert-and-unlink', {'id': request.env.user.partner_id.id})]
|
||||
reactions = message_sudo.env['mail.message.reaction'].search([('message_id', '=', message_sudo.id), ('content', '=', content)])
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'messageId': message_sudo.id,
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'partners': partners,
|
||||
})],
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Channel API
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -719,6 +719,55 @@ class Channel(models.Model):
|
||||
'body': message.body,
|
||||
},
|
||||
})
|
||||
return super()._message_update_content_after_hook(message=message)
|
||||
|
||||
def _message_add_reaction_after_hook(self, message, content):
|
||||
self.ensure_one()
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
guests = [('insert', {'id': self.env.context.get('guest').id})]
|
||||
partners = []
|
||||
else:
|
||||
guests = []
|
||||
partners = [('insert', {'id': self.env.user.partner_id.id})]
|
||||
reactions = self.env['mail.message.reaction'].sudo().search([('message_id', '=', message.id), ('content', '=', content)])
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), {
|
||||
'type': 'mail.message_update',
|
||||
'payload': {
|
||||
'id': message.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'messageId': message.id,
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'partners': partners,
|
||||
})],
|
||||
},
|
||||
})
|
||||
return super()._message_add_reaction_after_hook(message=message, content=content)
|
||||
|
||||
def _message_remove_reaction_after_hook(self, message, content):
|
||||
self.ensure_one()
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
guests = [('insert-and-unlink', {'id': self.env.context.get('guest').id})]
|
||||
partners = []
|
||||
else:
|
||||
guests = []
|
||||
partners = [('insert-and-unlink', {'id': self.env.user.partner_id.id})]
|
||||
reactions = self.env['mail.message.reaction'].sudo().search([('message_id', '=', message.id), ('content', '=', content)])
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), {
|
||||
'type': 'mail.message_update',
|
||||
'payload': {
|
||||
'id': message.id,
|
||||
'messageReactionGroups': [('insert' if len(reactions) > 0 else 'insert-and-unlink', {
|
||||
'messageId': message.id,
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'guests': guests,
|
||||
'partners': partners,
|
||||
})],
|
||||
},
|
||||
})
|
||||
return super()._message_remove_reaction_after_hook(message=message, content=content)
|
||||
|
||||
def _message_subscribe(self, partner_ids=None, subtype_ids=None, customer_ids=None):
|
||||
""" Do not allow follower subscription on channels. Only members are
|
||||
|
||||
@@ -176,6 +176,7 @@ class Message(models.Model):
|
||||
# Besides for new messages, and messages never sending emails, there was no mail, and it was searching for nothing.
|
||||
mail_ids = fields.One2many('mail.mail', 'mail_message_id', string='Mails', groups="base.group_system")
|
||||
canned_response_ids = fields.One2many('mail.shortcode', 'message_ids', string="Canned Responses", store=False)
|
||||
reaction_ids = fields.One2many('mail.message.reaction', 'message_id', string="Reactions", groups="base.group_system")
|
||||
|
||||
def _compute_description(self):
|
||||
for message in self:
|
||||
@@ -758,6 +759,40 @@ class Message(models.Model):
|
||||
notification = {'type': 'toggle_star', 'message_ids': [self.id], 'starred': starred}
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification)
|
||||
|
||||
def _message_add_reaction(self, content):
|
||||
self.ensure_one()
|
||||
self.check_access_rule('write')
|
||||
self.check_access_rights('write')
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
guest = self.env.context.get('guest')
|
||||
partner = self.env['res.partner']
|
||||
else:
|
||||
guest = self.env['mail.guest']
|
||||
partner = self.env.user.partner_id
|
||||
reaction = self.env['mail.message.reaction'].sudo().search([('message_id', '=', self.id), ('partner_id', '=', partner.id), ('guest_id', '=', guest.id), ('content', '=', content)])
|
||||
if not reaction:
|
||||
reaction = self.env['mail.message.reaction'].sudo().create({
|
||||
'message_id': self.id,
|
||||
'content': content,
|
||||
'partner_id': partner.id,
|
||||
'guest_id': guest.id,
|
||||
})
|
||||
self.env[self.model].browse(self.res_id)._message_add_reaction_after_hook(message=self, content=reaction.content)
|
||||
|
||||
def _message_remove_reaction(self, content):
|
||||
self.ensure_one()
|
||||
self.check_access_rule('write')
|
||||
self.check_access_rights('write')
|
||||
if self.env.user._is_public() and 'guest' in self.env.context:
|
||||
guest = self.env.context.get('guest')
|
||||
partner = self.env['res.partner']
|
||||
else:
|
||||
guest = self.env['mail.guest']
|
||||
partner = self.env.user.partner_id
|
||||
reaction = self.env['mail.message.reaction'].sudo().search([('message_id', '=', self.id), ('partner_id', '=', partner.id), ('guest_id', '=', guest.id), ('content', '=', content)])
|
||||
reaction.unlink()
|
||||
self.env[self.model].browse(self.res_id)._message_remove_reaction_after_hook(message=self, content=content)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# MESSAGE READ / FETCH / FAILURE API
|
||||
# ------------------------------------------------------
|
||||
@@ -818,10 +853,21 @@ class Message(models.Model):
|
||||
})]
|
||||
else:
|
||||
vals['author_id'] = author
|
||||
reactions_per_content = defaultdict(lambda: self.env['mail.message.reaction'])
|
||||
for reaction in message_sudo.reaction_ids:
|
||||
reactions_per_content[reaction.content] |= reaction
|
||||
reaction_groups = [('insert-and-replace', [{
|
||||
'messageId': message_sudo.id,
|
||||
'content': content,
|
||||
'count': len(reactions),
|
||||
'partners': [('insert-and-replace', [{'id': partner.id, 'name': partner.name} for partner in reactions.partner_id])],
|
||||
'guests': [('insert-and-replace', [{'id': guest.id, 'name': guest.name} for guest in reactions.guest_id])],
|
||||
} for content, reactions in reactions_per_content.items()])]
|
||||
vals.update({
|
||||
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
|
||||
'attachment_ids': attachments_formatted,
|
||||
'tracking_value_ids': tracking_value_ids,
|
||||
'messageReactionGroups': reaction_groups,
|
||||
'record_name': record_name,
|
||||
})
|
||||
|
||||
|
||||
@@ -1886,6 +1886,12 @@ class MailThread(models.AbstractModel):
|
||||
def _message_update_content_after_hook(self, message):
|
||||
""" Hook to add custom behavior after having updated the message content. """
|
||||
|
||||
def _message_add_reaction_after_hook(self, message, content):
|
||||
""" Hook to add custom behavior after having added a reaction to a message. """
|
||||
|
||||
def _message_remove_reaction_after_hook(self, message, content):
|
||||
""" Hook to add custom behavior after having removed a reaction from a message. """
|
||||
|
||||
def _check_can_update_message_content(self, message):
|
||||
"""" Checks that the current user can update the content of the message. """
|
||||
note_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
@@ -281,8 +281,7 @@ export class Composer extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when selection an emoji from the emoji popover (from the emoji
|
||||
* button).
|
||||
* Handles `o-emoji-selection` event from the emoji popover.
|
||||
*
|
||||
* @private
|
||||
* @param {CustomEvent} ev
|
||||
|
||||
@@ -1174,7 +1174,7 @@ QUnit.test('open channel from active_id as channel id', async function (assert)
|
||||
|
||||
QUnit.test('basic rendering of message', async function (assert) {
|
||||
// AKU TODO: should be in message-only tests
|
||||
assert.expect(13);
|
||||
assert.expect(14);
|
||||
|
||||
// channel expected to be rendered, with a random unique id that will be referenced in the test
|
||||
this.data['mail.channel'].records.push({ id: 20 });
|
||||
@@ -1253,14 +1253,19 @@ QUnit.test('basic rendering of message', async function (assert) {
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_action`).length,
|
||||
1,
|
||||
"should have a single action in action list of message"
|
||||
2,
|
||||
"should have 2 actions in action list of message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
1,
|
||||
"should have action to star message"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_MessageActionList_actionReaction',
|
||||
"should have action to add a reaction"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_content`).length,
|
||||
1,
|
||||
@@ -2876,7 +2881,7 @@ QUnit.test('do not post message on channel with "SHIFT-Enter" keyboard shortcut'
|
||||
});
|
||||
QUnit.test('rendering of inbox message', async function (assert) {
|
||||
// AKU TODO: kinda message specific test
|
||||
assert.expect(7);
|
||||
assert.expect(8);
|
||||
|
||||
this.data['mail.message'].records.push({
|
||||
body: "not empty",
|
||||
@@ -2925,8 +2930,13 @@ QUnit.test('rendering of inbox message', async function (assert) {
|
||||
await afterNextRender(() => message.click());
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_action`).length,
|
||||
3,
|
||||
"should display 3 actions"
|
||||
4,
|
||||
"should display 4 actions"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_MessageActionList_actionReaction',
|
||||
"should have action to add a reaction"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
|
||||
@@ -132,7 +132,11 @@ export class Message extends Component {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isActive() {
|
||||
return this.state.isHovered || this.state.isClicked;
|
||||
return Boolean(
|
||||
this.state.isHovered ||
|
||||
this.state.isClicked ||
|
||||
(this.message && this.message.actionList.isReactionPopoverOpened)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +474,8 @@ export class Message extends Component {
|
||||
!isEventHandled(ev, 'Message.ClickAuthorAvatar') &&
|
||||
!isEventHandled(ev, 'Message.ClickAuthorName') &&
|
||||
!isEventHandled(ev, 'Message.ClickFailure') &&
|
||||
!isEventHandled(ev, 'MessageActionList.Click')
|
||||
!isEventHandled(ev, 'MessageActionList.Click') &&
|
||||
!isEventHandled(ev, 'MessageReactionGroup.Click')
|
||||
) {
|
||||
this.state.isClicked = !this.state.isClicked;
|
||||
}
|
||||
|
||||
@@ -139,6 +139,11 @@
|
||||
</ul>
|
||||
</t>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ml-2">
|
||||
<t t-foreach="message.messageReactionGroups" t-as="messageReactionGroup" t-key="messageReactionGroup.localId">
|
||||
<MessageReactionGroup messageReactionGroupLocalId="messageReactionGroup.localId"/>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="message.subject and !message.isSubjectSimilarToOriginThreadName">
|
||||
<p class="o_Message_subject m-2 mt-0">Subject: <t t-esc="message.subject"/></p>
|
||||
</t>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class MessageActionList extends Component {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'reactionPopoverRef', modelName: 'mail.message_action_list', propNameAsRecordLocalId: 'actionListLocalId', refName: 'reactionPopover' });
|
||||
this.ADD_A_REACTION = this.env._t("Add a Reaction");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {mail.message}
|
||||
*/
|
||||
|
||||
@@ -20,3 +20,7 @@
|
||||
.o_MessageActionList_actionStar.o_MessageActionList_actionStar_active {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.o_MessageActionList_actionReaction {
|
||||
display: inline; // override block from popover div
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.MessageActionList" owl="1">
|
||||
<div class="o_MessageActionList" t-on-click="actionList.onClick">
|
||||
<div class="o_MessageActionList d-flex" t-on-click="actionList.onClick">
|
||||
<Popover class="o_MessageActionList_action o_MessageActionList_actionReaction p-2 fa fa-lg fa-smile-o" position="'top'" titleAttribute="ADD_A_REACTION" t-on-click="actionList.onClickReaction" t-on-o-emoji-selection="actionList.onEmojiSelection" t-on-o-popover-closed="actionList.onReactionPopoverClosed" t-on-o-popover-opened="actionList.onReactionPopoverOpened" t-ref="reactionPopover">
|
||||
<t t-set="opened">
|
||||
<EmojisPopover/>
|
||||
</t>
|
||||
</Popover>
|
||||
<span t-if="actionList.message.canStarBeToggled" class="o_MessageActionList_action o_MessageActionList_actionStar p-2" t-att-class="{
|
||||
'o_MessageActionList_actionStar_active': actionList.message.isStarred,
|
||||
'fa fa-lg fa-star': actionList.message.isStarred,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component } = owl;
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
export class MessageReactionGroup extends Component {
|
||||
|
||||
get messageReactionGroup() {
|
||||
return this.messaging.models['mail.message_reaction_group'].get(this.props.messageReactionGroupLocalId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(MessageReactionGroup, {
|
||||
props: {
|
||||
messageReactionGroupLocalId: String,
|
||||
},
|
||||
template: 'mail.MessageReactionGroup',
|
||||
});
|
||||
|
||||
registerMessagingComponent(MessageReactionGroup);
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_MessageReactionGroup {
|
||||
border-radius: $o-mail-rounded-rectangle-border-radius-sm;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_MessageReactionGroup {
|
||||
background: $white;
|
||||
border-color: gray('200');
|
||||
border-style: solid;
|
||||
border-width: $border-width;
|
||||
|
||||
&.o-hasUserReacted, &.o-hasUserReacted:hover {
|
||||
border-color: $o-brand-primary;
|
||||
background-color: rgba($o-brand-primary, .1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: gray('400');
|
||||
background-color: gray('200');
|
||||
}
|
||||
}
|
||||
|
||||
.o_MessageReactionGroup_count {
|
||||
|
||||
&.o-hasUserReacted {
|
||||
color: $o-brand-primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.MessageReactionGroup" owl="1">
|
||||
<div class="o_MessageReactionGroup d-flex mr-1 mb-2" t-att-class="{'o-hasUserReacted': messageReactionGroup and messageReactionGroup.hasUserReacted}" t-att-title="messageReactionGroup and messageReactionGroup.summary" t-on-click="messageReactionGroup and messageReactionGroup.onClick">
|
||||
<t t-if="messageReactionGroup">
|
||||
<span class="o_MessageReactionGroup_content mx-1">
|
||||
<t t-esc="messageReactionGroup.content"/>
|
||||
</span>
|
||||
<span class="o_MessageReactionGroup_count mx-1" t-att-class="{'o-hasUserReacted': messageReactionGroup.hasUserReacted}">
|
||||
<t t-esc="messageReactionGroup.count"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -74,6 +74,9 @@ function factory(dependencies) {
|
||||
if ('is_notification' in data) {
|
||||
data2.is_notification = data.is_notification;
|
||||
}
|
||||
if ('messageReactionGroups' in data) {
|
||||
data2.messageReactionGroups = data.messageReactionGroups;
|
||||
}
|
||||
if ('message_type' in data) {
|
||||
data2.message_type = data.message_type;
|
||||
}
|
||||
@@ -200,6 +203,22 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given reaction on this message.
|
||||
*
|
||||
* @param {string} content
|
||||
*/
|
||||
async addReaction(content) {
|
||||
const messageData = await this.env.services.rpc({
|
||||
route: '/mail/message/add_reaction',
|
||||
params: { content, message_id: this.id },
|
||||
});
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.update(messageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this message as read, so that it no longer appears in current
|
||||
* partner Inbox.
|
||||
@@ -233,6 +252,22 @@ function factory(dependencies) {
|
||||
this.update({ dateFromNow: this._computeDateFromNow() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given reaction from this message.
|
||||
*
|
||||
* @param {string} content
|
||||
*/
|
||||
async removeReaction(content) {
|
||||
const messageData = await this.env.services.rpc({
|
||||
route: '/mail/message/remove_reaction',
|
||||
params: { content, message_id: this.id },
|
||||
});
|
||||
if (!this.exists()) {
|
||||
return;
|
||||
}
|
||||
this.update(messageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to initiate reply to current message in Discuss Inbox. Assumes
|
||||
* that Discuss and Inbox are already opened.
|
||||
@@ -639,6 +674,14 @@ function factory(dependencies) {
|
||||
isStarred: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Groups of reactions per content allowing to know the number of
|
||||
* reactions for each.
|
||||
*/
|
||||
messageReactionGroups: one2many('mail.message_reaction_group', {
|
||||
inverse: 'message',
|
||||
isCausal: true,
|
||||
}),
|
||||
message_type: attr(),
|
||||
notifications: one2many('mail.notification', {
|
||||
inverse: 'message',
|
||||
|
||||
@@ -14,18 +14,17 @@ function factory(dependencies) {
|
||||
_created() {
|
||||
// bind handlers so they can be used in templates
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.onClickReplyTo = this.onClickReplyTo.bind(this);
|
||||
this.onClickConfirmDelete = this.onClickConfirmDelete.bind(this);
|
||||
this.onClickDelete = this.onClickDelete.bind(this);
|
||||
this.onClickMarkAsRead = this.onClickMarkAsRead.bind(this);
|
||||
this.onReactionPopoverOpened = this.onReactionPopoverOpened.bind(this);
|
||||
this.onReactionPopoverClosed = this.onReactionPopoverClosed.bind(this);
|
||||
this.onClickReplyTo = this.onClickReplyTo.bind(this);
|
||||
this.onClickToggleStar = this.onClickToggleStar.bind(this);
|
||||
this.onDeleteConfirmDialogClosed = this.onDeleteConfirmDialogClosed.bind(this);
|
||||
this.onEmojiSelection = this.onEmojiSelection.bind(this);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
@@ -58,6 +57,22 @@ function factory(dependencies) {
|
||||
this.message.markAsRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onReactionPopoverClosed(ev) {
|
||||
this.update({ isReactionPopoverOpened: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onReactionPopoverOpened(ev) {
|
||||
this.update({ isReactionPopoverOpened: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the reply composer for this message (or closes it if it was
|
||||
* already opened).
|
||||
@@ -83,15 +98,48 @@ function factory(dependencies) {
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onDeleteConfirmDialogClosed(ev) {
|
||||
this.update({ showDeleteConfirm: false })
|
||||
this.update({ showDeleteConfirm: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles `o-emoji-selection` event from the emoji popover.
|
||||
*
|
||||
* @private
|
||||
* @param {CustomEvent} ev
|
||||
* @param {Object} ev.detail
|
||||
* @param {string} ev.detail.unicode
|
||||
*/
|
||||
onEmojiSelection(ev) {
|
||||
this.message.addReaction(ev.detail.unicode);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
_computeIsReactionPopoverOpened() {
|
||||
return Boolean(
|
||||
this.reactionPopoverRef &&
|
||||
this.reactionPopoverRef.comp &&
|
||||
this.reactionPopoverRef.comp.state.displayed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageActionList.fields = {
|
||||
/**
|
||||
* States whether the reaction popover is currently opened.
|
||||
*/
|
||||
isReactionPopoverOpened: attr({
|
||||
compute: '_computeIsReactionPopoverOpened',
|
||||
}),
|
||||
message: one2one('mail.message', {
|
||||
inverse: 'actionList'
|
||||
}),
|
||||
/**
|
||||
* States the reference to the reaction popover component (if any).
|
||||
*/
|
||||
reactionPopoverRef: attr(),
|
||||
/**
|
||||
* Whether to show the message delete-confirm dialog
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerNewModel } from '@mail/model/model_core';
|
||||
import { attr, many2many, many2one } from '@mail/model/model_field';
|
||||
import { insertAndReplace } from '@mail/model/model_field_command';
|
||||
import { markEventHandled } from '@mail/utils/utils';
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
class MessageReactionGroup extends dependencies['mail.model'] {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_created() {
|
||||
super._created();
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles click on the reaction group.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClick(ev) {
|
||||
markEventHandled(ev, 'MessageReactionGroup.Click');
|
||||
if (this.hasUserReacted) {
|
||||
this.message.removeReaction(this.content);
|
||||
} else {
|
||||
this.message.addReaction(this.content);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
static _createRecordLocalId(data) {
|
||||
return `${this.modelName}_${data.messageId}_${data.content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeHasUserReacted() {
|
||||
return Boolean(
|
||||
(this.messaging.currentPartner && this.partners.includes(this.messaging.currentPartner)) ||
|
||||
(this.messaging.currentGuest && this.guests.includes(this.messaging.currentGuest))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
*/
|
||||
_computeMessage() {
|
||||
return insertAndReplace({ id: this.messageId });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
_computeSummary() {
|
||||
const {
|
||||
length,
|
||||
0: firstUser,
|
||||
1: secondUser,
|
||||
2: thirdUser,
|
||||
} = [
|
||||
...this.partners.map(partner => partner.name),
|
||||
...this.guests.map(guest => guest.name),
|
||||
];
|
||||
switch (length) {
|
||||
case 1:
|
||||
return _.str.sprintf(this.env._t('%s has reacted with %s'), firstUser, this.content);
|
||||
case 2:
|
||||
return _.str.sprintf(this.env._t('%s and %s have reacted with %s'), firstUser, secondUser, this.content);
|
||||
case 3:
|
||||
return _.str.sprintf(this.env._t('%s, %s, %s have reacted with %s'), firstUser, secondUser, thirdUser, this.content);
|
||||
case 4:
|
||||
return _.str.sprintf(this.env._t('%s, %s, %s and 1 other person have reacted with %s'), firstUser, secondUser, thirdUser, this.content);
|
||||
default:
|
||||
return _.str.sprintf(this.env._t('%s, %s, %s and %s other persons have reacted with %s'), firstUser, secondUser, thirdUser, length - 3, this.content);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MessageReactionGroup.fields = {
|
||||
content: attr({
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
count: attr({
|
||||
required: true,
|
||||
}),
|
||||
/**
|
||||
* States the guests that have used this reaction on this message.
|
||||
*/
|
||||
guests: many2many('mail.guest'),
|
||||
hasUserReacted: attr({
|
||||
compute: '_computeHasUserReacted',
|
||||
default: false,
|
||||
}),
|
||||
message: many2one('mail.message', {
|
||||
compute: '_computeMessage',
|
||||
inverse: 'messageReactionGroups',
|
||||
required: true,
|
||||
}),
|
||||
messageId: attr({
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
/**
|
||||
* States the partners that have used this reaction on this message.
|
||||
*/
|
||||
partners: many2many('mail.partner'),
|
||||
summary: attr({
|
||||
compute: '_computeSummary',
|
||||
}),
|
||||
};
|
||||
|
||||
MessageReactionGroup.modelName = 'mail.message_reaction_group';
|
||||
|
||||
return MessageReactionGroup;
|
||||
}
|
||||
|
||||
registerNewModel('mail.message_reaction_group', factory);
|
||||
@@ -835,7 +835,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
messages.flush()
|
||||
messages.invalidate_cache()
|
||||
|
||||
with self.assertQueryCount(emp=16):
|
||||
with self.assertQueryCount(emp=17):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 2)
|
||||
for message in res:
|
||||
@@ -863,7 +863,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
messages.flush()
|
||||
messages.invalidate_cache()
|
||||
|
||||
with self.assertQueryCount(emp=12):
|
||||
with self.assertQueryCount(emp=13):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 6)
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ odoo.define('web.Popover', function (require) {
|
||||
|
||||
_close() {
|
||||
this.state.displayed = false;
|
||||
this.trigger('o-popover-closed');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +172,11 @@ odoo.define('web.Popover', function (require) {
|
||||
*/
|
||||
_onClick(ev) {
|
||||
this.state.displayed = !this.state.displayed;
|
||||
if (this.state.displayed) {
|
||||
this.trigger('o-popover-opened');
|
||||
} else {
|
||||
this.trigger('o-popover-closed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,6 +328,7 @@ odoo.define('web.Popover', function (require) {
|
||||
validate: (p) => ['top', 'bottom', 'left', 'right'].includes(p),
|
||||
},
|
||||
title: { type: String, optional: true },
|
||||
titleAttribute: { type: String, optional: true },
|
||||
};
|
||||
|
||||
QWeb.registerComponent('Popover', Popover);
|
||||
|
||||
@@ -509,7 +509,7 @@
|
||||
</t>
|
||||
|
||||
<t t-name="Popover" owl="1">
|
||||
<div t-att-class="{ 'o_is_open': state.displayed }" t-on-click="_onClick" t-on-o-popover-compute="_onPopoverCompute" t-on-o-popover-close="_onPopoverClose">
|
||||
<div t-att-class="{ 'o_is_open': state.displayed }" t-att-title="props.titleAttribute" t-on-click="_onClick" t-on-o-popover-compute="_onPopoverCompute" t-on-o-popover-close="_onPopoverClose">
|
||||
<t t-slot="default"/>
|
||||
<Portal t-if="state.displayed" target="'body'">
|
||||
<div role="tooltip" class="o_popover" t-att-class="props.popoverClass" t-ref="popover">
|
||||
|
||||
Reference in New Issue
Block a user