[IMP] mail: allow user to delete their messages
This commit allows users to "delete" their messages in discuss, as well as their notes on other records. Admins can delete anyone discuss messages or notes. Tracking messages such as a change in stage or other cannot be deleted. This "delete" is a "soft-delete" in that it only empties the message's content, so that it is no longer displayed in the UI, but remains in the database so that people who got notified by email can still reply to the message, among other things. task-2365646 closes odoo/odoo#74857 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
e67a123227
commit
776d1ee08b
@@ -128,6 +128,18 @@ class DiscussController(http.Controller):
|
||||
allowed_params = {'attachment_ids', 'body', 'message_type', 'partner_ids', 'subtype_xmlid'}
|
||||
return thread.message_post(**{key: value for key, value in post_data.items() if key in allowed_params}).message_format()[0]
|
||||
|
||||
@http.route('/mail/message/update_content', methods=['POST'], type='json', auth='public')
|
||||
def mail_message_update_content(self, message_id, body):
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
message_sudo = guest.env['mail.message'].browse(message_id).sudo().exists()
|
||||
if not message_sudo.is_current_user_or_guest_author and not guest.env.user.has_group('base.group_system'):
|
||||
raise NotFound()
|
||||
message_sudo._update_content(body=body)
|
||||
return {
|
||||
'id': message_sudo.id,
|
||||
'body': message_sudo.body,
|
||||
}
|
||||
|
||||
@http.route('/mail/attachment/upload', methods=['POST'], type='http', auth='public')
|
||||
def mail_attachment_upload(self, ufile, thread_id, thread_model, is_pending=False, **kwargs):
|
||||
channel_partner = request.env['mail.channel.partner']
|
||||
@@ -176,11 +188,10 @@ class DiscussController(http.Controller):
|
||||
# For non-internal users 2 cases are supported:
|
||||
# - Either the attachment is linked to a message: verify the request is made by the author of the message (portal user or guest).
|
||||
# - Either a valid access token is given: also verify the message is pending (because unfortunately in portal a token is also provided to guest for viewing others' attachments).
|
||||
message_sudo = request.env['mail.message'].sudo().search([('attachment_ids', 'in', attachment_sudo.ids)], limit=1)
|
||||
guest = request.env['mail.guest']._get_guest_from_request(request)
|
||||
message_sudo = guest.env['mail.message'].sudo().search([('attachment_ids', 'in', attachment_sudo.ids)], limit=1)
|
||||
if message_sudo:
|
||||
if request.session.uid and (not message_sudo.author_id or message_sudo.author_id != request.env.user.partner_id):
|
||||
raise NotFound()
|
||||
if not request.session.uid and (not message_sudo.author_guest_id or message_sudo.author_guest_id != request.env['mail.guest']._get_guest_from_request(request)):
|
||||
if not message_sudo.is_current_user_or_guest_author:
|
||||
raise NotFound()
|
||||
else:
|
||||
if not access_token or not attachment_sudo.access_token or not consteq(access_token, attachment_sudo.access_token):
|
||||
|
||||
@@ -533,6 +533,22 @@ class Channel(models.Model):
|
||||
self._set_last_seen_message(message)
|
||||
return super()._message_post_after_hook(message=message, msg_vals=msg_vals)
|
||||
|
||||
def _check_can_update_message_content(self, message):
|
||||
""" We don't call super in this override as we want to ignore the
|
||||
mail.thread behavior completely """
|
||||
if not message.message_type == 'comment':
|
||||
raise UserError(_("Only messages type comment can have their content updated on model 'mail.channel'"))
|
||||
|
||||
def _message_update_content_after_hook(self, message):
|
||||
self.ensure_one()
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), {
|
||||
'type': 'mail.message_update',
|
||||
'payload': {
|
||||
'id': message.id,
|
||||
'body': message.body,
|
||||
},
|
||||
})
|
||||
|
||||
def _message_subscribe(self, partner_ids=None, subtype_ids=None, customer_ids=None):
|
||||
""" Do not allow follower subscription on channels. Only members are
|
||||
considered. """
|
||||
|
||||
@@ -125,6 +125,7 @@ class Message(models.Model):
|
||||
help="Author of the message. If not set, email_from may hold an email address that did not match any partner.")
|
||||
author_avatar = fields.Binary("Author's avatar", related='author_id.avatar_128', depends=['author_id'], readonly=False)
|
||||
author_guest_id = fields.Many2one(string="Guest", comodel_name='mail.guest')
|
||||
is_current_user_or_guest_author = fields.Boolean(compute='_compute_is_current_user_or_guest_author')
|
||||
# recipients: include inactive partners (they may have been archived after
|
||||
# the message was sent, but they should remain visible in the relation)
|
||||
partner_ids = fields.Many2many('res.partner', string='Recipients', context={'active_test': False})
|
||||
@@ -184,6 +185,18 @@ class Message(models.Model):
|
||||
plaintext_ct = '' if not message.body else tools.html2plaintext(message.body)
|
||||
message.description = plaintext_ct[:30] + '%s' % (' [...]' if len(plaintext_ct) >= 30 else '')
|
||||
|
||||
@api.depends('author_id', 'author_guest_id')
|
||||
@api.depends_context('guest', 'uid')
|
||||
def _compute_is_current_user_or_guest_author(self):
|
||||
user = self.env.user
|
||||
for message in self:
|
||||
if not user._is_public() and (message.author_id and message.author_id == user.partner_id):
|
||||
message.is_current_user_or_guest_author = True
|
||||
elif user._is_public() and (message.author_guest_id and message.author_guest_id == self.env.context.get('guest')):
|
||||
message.is_current_user_or_guest_author = True
|
||||
else:
|
||||
message.is_current_user_or_guest_author = False
|
||||
|
||||
def _compute_needaction(self):
|
||||
""" Need action on a mail.message = notified on my channel """
|
||||
my_messages = self.env['mail.notification'].sudo().search([
|
||||
@@ -660,6 +673,13 @@ class Message(models.Model):
|
||||
|
||||
return super(Message, self).export_data(fields_to_export)
|
||||
|
||||
def _update_content(self, body):
|
||||
self.ensure_one()
|
||||
thread = self.env[self.model].browse(self.res_id)
|
||||
thread._check_can_update_message_content(self)
|
||||
self.body = body
|
||||
thread._message_update_content_after_hook(self)
|
||||
|
||||
# ------------------------------------------------------
|
||||
# DISCUSS API
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -1882,7 +1882,15 @@ class MailThread(models.AbstractModel):
|
||||
""" Hook to add custom behavior after having posted the message. Both
|
||||
message and computed value are given, to try to lessen query count by
|
||||
using already-computed values instead of having to rebrowse things. """
|
||||
pass
|
||||
|
||||
def _message_update_content_after_hook(self, message):
|
||||
""" Hook to add custom behavior after having updated the message content. """
|
||||
|
||||
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')
|
||||
if not message.subtype_id.id == note_id:
|
||||
raise exceptions.UserError(_("Only logged notes can have their content updated on model '%s'", self._name))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# MESSAGE POST TOOLS
|
||||
|
||||
@@ -208,10 +208,15 @@ export class Composer extends Component {
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickCaptureGlobal(ev) {
|
||||
async _onClickCaptureGlobal(ev) {
|
||||
if (this.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
// Let event be handled by bubbling handlers first
|
||||
await new Promise(this.env.browser.setTimeout);
|
||||
if (isEventHandled(ev, 'MessageActionList.replyTo')) {
|
||||
return;
|
||||
}
|
||||
if (!this.composer) {
|
||||
return;
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import Dialog from 'web.OwlDialog';
|
||||
|
||||
const { Component } = owl;
|
||||
const { useRef } = owl.hooks;
|
||||
|
||||
export class DeleteMessageConfirmDialog extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.title = this.env._t("Confirmation");
|
||||
this.dialogRef = useRef('dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {mail.message}
|
||||
*/
|
||||
get actionList() {
|
||||
return this.messaging && this.messaging.models['mail.message_action_list'].get(this.props.actionListLocalId);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(DeleteMessageConfirmDialog, {
|
||||
components: {
|
||||
Dialog,
|
||||
},
|
||||
props: {
|
||||
actionListLocalId: String,
|
||||
},
|
||||
template: 'mail.DeleteMessageConfirmDialog',
|
||||
});
|
||||
|
||||
registerMessagingComponent(DeleteMessageConfirmDialog);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.o_DeleteMessageConfirmDialog_blockquote {
|
||||
font-style: normal;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.DeleteMessageConfirmDialog" owl="1">
|
||||
<Dialog contentClass="'o_DeleteMessageConfirmDialog'" title="title" t-on-dialog-closed="actionList.onDeleteConfirmDialogClosed" t-ref="dialog">
|
||||
<p>Are you sure you want to delete this message?</p>
|
||||
<blockquote class="o_DeleteMessageConfirmDialog_blockquote">
|
||||
<Message messageLocalId="actionList.message.localId" showActions="false"/>
|
||||
</blockquote>
|
||||
<small t-if="!actionList.message.originThread or actionList.message.originThread.model !== 'mail.channel'">Pay attention: The followers of this document who were notified by email will still be able to read the content of this message and reply to it.</small>
|
||||
<t t-set-slot="buttons">
|
||||
<button class="btn btn-primary" t-on-click="actionList.onClickConfirmDelete">Delete</button>
|
||||
<button class="btn btn-secondary" t-on-click="dialogRef.comp._close()">Cancel</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -74,8 +74,9 @@ QUnit.test('reply: discard on pressing escape', async function (assert) {
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -192,9 +193,10 @@ QUnit.test('reply: discard on discard button click', async function (assert) {
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -252,18 +254,18 @@ QUnit.test('reply: discard on reply button toggle', async function (assert) {
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Composer',
|
||||
"should have composer after clicking on reply to message"
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`.o_Message_commandReply`).click()
|
||||
document.querySelector(`.o_MessageActionList_actionReply`).click()
|
||||
);
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
@@ -307,9 +309,10 @@ QUnit.test('reply: discard on click away', async function (assert) {
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
@@ -410,9 +413,10 @@ QUnit.test('"reply to" composer should log note if message replied to is a note'
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_Composer_buttonSend').textContent.trim(),
|
||||
@@ -481,9 +485,10 @@ QUnit.test('"reply to" composer should send message if message replied to is not
|
||||
'.o_Message',
|
||||
"should display a single message"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_Composer_buttonSend').textContent.trim(),
|
||||
|
||||
@@ -1243,20 +1243,23 @@ QUnit.test('basic rendering of message', async function (assert) {
|
||||
1,
|
||||
"should have date in header of message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_header .o_Message_commands`).length,
|
||||
1,
|
||||
"should have commands in header of message"
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message').click()
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_header .o_Message_command`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList`).length,
|
||||
1,
|
||||
"should have a single command in header of message"
|
||||
"should action list in message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_commandStar`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_action`).length,
|
||||
1,
|
||||
"should have command to star message"
|
||||
"should have a single action in action list of message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
1,
|
||||
"should have action to star message"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_content`).length,
|
||||
@@ -1345,20 +1348,23 @@ QUnit.test('basic rendering of squashed message', async function (assert) {
|
||||
message2.querySelector(`:scope .o_Message_sidebar`).classList.contains('o-message-squashed'),
|
||||
"message 2 should have squashed sidebar"
|
||||
);
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message.o-squashed').click()
|
||||
);
|
||||
assert.strictEqual(
|
||||
message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_date`).length,
|
||||
1,
|
||||
"message 2 should have date in sidebar"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commands`).length,
|
||||
message2.querySelectorAll(`:scope .o_MessageActionList`).length,
|
||||
1,
|
||||
"message 2 should have some commands in sidebar"
|
||||
"message 2 should have some actions"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message2.querySelectorAll(`:scope .o_Message_sidebar .o_Message_commandStar`).length,
|
||||
message2.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
1,
|
||||
"message 2 should have star command in sidebar"
|
||||
"message 2 should have star action in action list"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message2.querySelectorAll(`:scope .o_Message_core`).length,
|
||||
@@ -2489,13 +2495,14 @@ QUnit.test('toggle_star message', async function (assert) {
|
||||
message.classList.contains('o-starred'),
|
||||
"message should not be starred"
|
||||
);
|
||||
await afterNextRender(() => message.click());
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_commandStar`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
1,
|
||||
"message should have star command"
|
||||
"message should have star action"
|
||||
);
|
||||
|
||||
await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click());
|
||||
await afterNextRender(() => message.querySelector(`:scope .o_MessageActionList_actionStar`).click());
|
||||
assert.verifySteps(['rpc:toggle_message_starred']);
|
||||
assert.strictEqual(
|
||||
document.querySelector(`
|
||||
@@ -2518,7 +2525,7 @@ QUnit.test('toggle_star message', async function (assert) {
|
||||
"message should be starred"
|
||||
);
|
||||
|
||||
await afterNextRender(() => message.querySelector(`:scope .o_Message_commandStar`).click());
|
||||
await afterNextRender(() => message.querySelector(`:scope .o_MessageActionList_actionStar`).click());
|
||||
assert.verifySteps(['rpc:toggle_message_starred']);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(`
|
||||
@@ -2915,25 +2922,26 @@ QUnit.test('rendering of inbox message', async function (assert) {
|
||||
" on Refactoring",
|
||||
"should display origin thread name"
|
||||
);
|
||||
await afterNextRender(() => message.click());
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_command`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_action`).length,
|
||||
3,
|
||||
"should display 3 commands"
|
||||
"should display 3 actions"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_commandStar`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionStar`).length,
|
||||
1,
|
||||
"should display star command"
|
||||
"should display star action"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_commandReply`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionReply`).length,
|
||||
1,
|
||||
"should display reply command"
|
||||
"should display reply action"
|
||||
);
|
||||
assert.strictEqual(
|
||||
message.querySelectorAll(`:scope .o_Message_commandMarkAsRead`).length,
|
||||
message.querySelectorAll(`:scope .o_MessageActionList_actionMarkRead`).length,
|
||||
1,
|
||||
"should display mark as read command"
|
||||
"should display mark as read action"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3209,9 +3217,10 @@ QUnit.test('reply to message from inbox (message linked to document)', async fun
|
||||
" on Refactoring",
|
||||
"should display message originates from record 'Refactoring'"
|
||||
);
|
||||
await afterNextRender(() => document.querySelector('.o_Message').click());
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_Message_commandReply').click()
|
||||
document.querySelector('.o_MessageActionList_actionReply').click()
|
||||
);
|
||||
assert.ok(
|
||||
document.querySelector('.o_Message').classList.contains('o-selected'),
|
||||
@@ -3456,12 +3465,19 @@ QUnit.test('mark a single message as read should only move this message to "Hist
|
||||
2,
|
||||
"inbox mailbox should have 2 messages"
|
||||
);
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`
|
||||
.o_Message[data-message-local-id="${
|
||||
this.messaging.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId
|
||||
}"]
|
||||
`).click()
|
||||
);
|
||||
|
||||
await afterNextRender(() =>
|
||||
document.querySelector(`
|
||||
.o_Message[data-message-local-id="${
|
||||
this.messaging.models['mail.message'].findFromIdentifyingData({ id: 1 }).localId
|
||||
}"] .o_Message_commandMarkAsRead
|
||||
}"] .o_MessageActionList_actionMarkRead
|
||||
`).click()
|
||||
);
|
||||
assert.containsOnce(
|
||||
|
||||
@@ -22,17 +22,18 @@ export class Message extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = useState({
|
||||
/**
|
||||
* Determine whether the message is hovered. When message is hovered
|
||||
* it displays message actions.
|
||||
*/
|
||||
isHovered: false,
|
||||
/**
|
||||
* Determine whether the message is clicked. When message is in
|
||||
* clicked state, it keeps displaying the commands.
|
||||
* clicked state, it keeps displaying actions even if not hovered.
|
||||
*/
|
||||
isClicked: false,
|
||||
});
|
||||
useUpdate({ func: () => this._update() });
|
||||
/**
|
||||
* The intent of the reply button depends on the last rendered state.
|
||||
*/
|
||||
this._wasSelected;
|
||||
/**
|
||||
* Value of the last rendered prettyBody. Useful to compare to new value
|
||||
* to decide if it has to be updated.
|
||||
@@ -124,6 +125,16 @@ export class Message extends Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the message is "active", ie: hovered or clicked, and should
|
||||
* display additional things (date in sidebar, message actions, etc.)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isActive() {
|
||||
return this.state.isHovered || this.state.isClicked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell whether the bottom of this message is visible or not.
|
||||
*
|
||||
@@ -419,7 +430,6 @@ export class Message extends Component {
|
||||
message: this.message,
|
||||
});
|
||||
}
|
||||
this._wasSelected = this.isSelected;
|
||||
this.message.refreshDateFromNow();
|
||||
clearInterval(this._intervalId);
|
||||
this._intervalId = setInterval(() => {
|
||||
@@ -459,7 +469,8 @@ export class Message extends Component {
|
||||
if (
|
||||
!isEventHandled(ev, 'Message.ClickAuthorAvatar') &&
|
||||
!isEventHandled(ev, 'Message.ClickAuthorName') &&
|
||||
!isEventHandled(ev, 'Message.ClickFailure')
|
||||
!isEventHandled(ev, 'Message.ClickFailure') &&
|
||||
!isEventHandled(ev, 'MessageActionList.Click')
|
||||
) {
|
||||
this.state.isClicked = !this.state.isClicked;
|
||||
}
|
||||
@@ -507,40 +518,6 @@ export class Message extends Component {
|
||||
ev.preventDefault();
|
||||
this.message.originThread.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickStar(ev) {
|
||||
ev.stopPropagation();
|
||||
this.message.toggleStar();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickMarkAsRead(ev) {
|
||||
ev.stopPropagation();
|
||||
this.message.markAsRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickReply(ev) {
|
||||
// Use this._wasSelected because this.props.isSelected might be changed
|
||||
// by a global capture click handler (for example the one from Composer)
|
||||
// before the current handler is executed. Indeed because it does a
|
||||
// toggle it needs to take into account the value before the click.
|
||||
if (this._wasSelected) {
|
||||
this.messaging.discuss.clearReplyingToMessage();
|
||||
} else {
|
||||
this.message.replyTo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(Message, {
|
||||
@@ -548,6 +525,7 @@ Object.assign(Message, {
|
||||
hasMarkAsReadIcon: false,
|
||||
hasReplyIcon: false,
|
||||
isSquashed: false,
|
||||
showActions: true,
|
||||
},
|
||||
props: {
|
||||
attachmentsDetailsMode: {
|
||||
@@ -563,6 +541,7 @@ Object.assign(Message, {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
showActions: Boolean,
|
||||
},
|
||||
template: 'mail.Message',
|
||||
});
|
||||
|
||||
@@ -27,19 +27,10 @@
|
||||
margin-inline-end: map-get($spacers, 2);
|
||||
}
|
||||
|
||||
.o_Message_commandStar {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.o_Message_Composer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.o_Message_commands {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.o_Message_content {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
@@ -79,29 +70,6 @@
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.o_Message_headerCommands {
|
||||
margin-inline-end: map-get($spacers, 2);
|
||||
align-self: center;
|
||||
|
||||
.o_Message_headerCommand {
|
||||
padding-left: map-get($spacers, 2);
|
||||
padding-right: map-get($spacers, 2);
|
||||
|
||||
&.o-mobile {
|
||||
padding-left: map-get($spacers, 3);
|
||||
padding-right: map-get($spacers, 3);
|
||||
|
||||
&:first-child {
|
||||
padding-left: map-get($spacers, 2);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: map-get($spacers, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_headerDate {
|
||||
margin-inline-end: map-get($spacers, 2);
|
||||
font-size: 0.8em;
|
||||
@@ -143,21 +111,12 @@
|
||||
max-width: $o-mail-message-sidebar-width;
|
||||
display: flex;
|
||||
margin-top: map-get($spacers, 2);
|
||||
margin-inline-end: map-get($spacers, 2);
|
||||
justify-content: center;
|
||||
|
||||
&.o-message-squashed {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_sidebarItem {
|
||||
margin-left: map-get($spacers, 1);
|
||||
margin-right: map-get($spacers, 1);
|
||||
|
||||
&.o-message-squashed {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_trackingValues {
|
||||
@@ -181,21 +140,6 @@
|
||||
.o_Message {
|
||||
background-color: $white;
|
||||
|
||||
&:hover, &.o-clicked {
|
||||
|
||||
.o_Message_commands {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_Message_sidebarItem.o-message-squashed {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.o_Message_seenIndicator.o-message-squashed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_partnerImStatusIcon {
|
||||
color: $white;
|
||||
}
|
||||
@@ -220,17 +164,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.o-starred {
|
||||
|
||||
.o_Message_commandStar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.o_Message_commands {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.o-has-message-selection:not(.o-selected) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -248,40 +181,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_Message_command {
|
||||
cursor: pointer;
|
||||
color: gray('400');
|
||||
|
||||
&:not(.o-mobile) {
|
||||
&:hover {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&.o-mobile {
|
||||
filter: brightness(0.8);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
&.o-message-selected {
|
||||
color: gray('500');
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_commandStar {
|
||||
|
||||
&.o-message-starred {
|
||||
color: gold;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_content .o_mention {
|
||||
color: $o-brand-primary;
|
||||
cursor: pointer;
|
||||
@@ -299,8 +198,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_headerCommands:not(.o-mobile) {
|
||||
opacity: 0;
|
||||
.o_Message_contentWrapper {
|
||||
// transparent border to avoid layout moving around when it becomes active
|
||||
border: $border-width solid transparent;
|
||||
border-radius: $o-mail-rounded-rectangle-border-radius-sm;
|
||||
|
||||
&.o_Message_contentWrapper_active {
|
||||
border: $border-width solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_highlightIndicator.o-active {
|
||||
@@ -333,14 +238,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_Message_sidebarCommands {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o_Message_sidebarItem.o-message-squashed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o_Message_subject {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
'o-selected': isSelected,
|
||||
'o-squashed': props.isSquashed,
|
||||
'o-starred': message and message.isStarred,
|
||||
}" t-on-click="_onClick" t-att-data-message-local-id="message and message.localId"
|
||||
}" t-on-click="_onClick" t-on-mouseenter="state.isHovered = true" t-on-mouseleave="state.isHovered = false" t-att-data-message-local-id="message and message.localId"
|
||||
>
|
||||
<t t-if="message" name="rootCondition">
|
||||
<div class="o_Message_highlightIndicator" t-att-class="{ 'o-active': message.isHighlighted }"/>
|
||||
<div class="o_Message_sidebar" t-att-class="{ 'o-message-squashed': props.isSquashed }">
|
||||
<div class="o_Message_sidebar" t-att-class="{ 'o-message-squashed align-items-start': props.isSquashed }">
|
||||
<t t-if="!props.isSquashed">
|
||||
<div class="o_Message_authorAvatarContainer o_Message_sidebarItem">
|
||||
<img class="o_Message_authorAvatar rounded-circle" t-att-class="{ o_Message_authorRedirect: hasAuthorOpenChat, o_redirect: hasAuthorOpenChat }" t-att-src="avatar" t-on-click="_onClickAuthorAvatar" t-att-title="hasAuthorOpenChat ? OPEN_CHAT : ''" alt="Avatar"/>
|
||||
@@ -36,32 +36,19 @@
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="message.date">
|
||||
<div class="o_Message_date o_Message_sidebarItem o-message-squashed" t-att-class="{ 'o-message-selected': isSelected }">
|
||||
<t t-if="isActive and message.date">
|
||||
<div class="o_Message_date o_Message_sidebarItem d-flex" t-att-class="{ 'o-message-selected': isSelected }">
|
||||
<t t-esc="shortTime"/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_Message_commands o_Message_sidebarCommands o_Message_sidebarItem o-message-squashed" t-att-class="{ 'o-message-selected': isSelected, 'o-mobile': messaging.device.isMobile }">
|
||||
<t t-if="message.message_type !== 'notification'">
|
||||
<div class="o_Message_command o_Message_commandStar fa"
|
||||
t-att-class="{
|
||||
'fa-star': message.isStarred,
|
||||
'fa-star-o': !message.isStarred,
|
||||
'o-message-selected': isSelected,
|
||||
'o-message-starred': message.isStarred,
|
||||
'o-mobile': messaging.device.isMobile,
|
||||
}" t-on-click="_onClickStar"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="message.isCurrentPartnerAuthor and threadView and threadView.thread and threadView.thread.hasSeenIndicators">
|
||||
<MessageSeenIndicator class="o_Message_seenIndicator o-message-squashed" messageLocalId="message.localId" threadLocalId="threadView.thread.localId"/>
|
||||
<t t-if="!isActive and message.isCurrentUserOrGuestAuthor and threadView and threadView.thread and threadView.thread.hasSeenIndicators">
|
||||
<MessageSeenIndicator class="o_Message_seenIndicator" messageLocalId="message.localId" threadLocalId="threadView.thread.localId"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_Message_core">
|
||||
<div class="o_Message_core flex-grow-1">
|
||||
<t t-if="!props.isSquashed">
|
||||
<div class="o_Message_header">
|
||||
<div class="o_Message_header ml-2">
|
||||
<t t-if="message.author">
|
||||
<div class="o_Message_authorName o_Message_authorRedirect o_redirect text-truncate" t-on-click="_onClickAuthorName" title="Open profile">
|
||||
<t t-esc="message.author.nameOrDisplayName"/>
|
||||
@@ -87,7 +74,7 @@
|
||||
- <t t-esc="message.dateFromNow"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="message.isCurrentPartnerAuthor and threadView and threadView.thread and threadView.thread.hasSeenIndicators">
|
||||
<t t-if="message.isCurrentUserOrGuestAuthor and threadView and threadView.thread and threadView.thread.hasSeenIndicators">
|
||||
<MessageSeenIndicator class="o_Message_seenIndicator" messageLocalId="message.localId" threadLocalId="threadView.thread.localId"/>
|
||||
</t>
|
||||
<t t-if="threadView and message.originThread and message.originThread !== threadView.thread">
|
||||
@@ -119,75 +106,54 @@
|
||||
</Popover>
|
||||
</t>
|
||||
</t>
|
||||
<div class="o_Message_commands o_Message_headerCommands" t-att-class="{ 'o-mobile': messaging.device.isMobile }">
|
||||
<t t-if="!message.isTemporary and ((message.message_type !== 'notification' and message.originThread and message.originThread.model === 'mail.channel') or !message.isTransient)">
|
||||
<span class="o_Message_command o_Message_commandStar o_Message_headerCommand fa"
|
||||
t-att-class="{
|
||||
'fa-star': message.isStarred,
|
||||
'fa-star-o': !message.isStarred,
|
||||
'o-message-selected': isSelected,
|
||||
'o-message-starred': message.isStarred,
|
||||
'o-mobile': messaging.device.isMobile,
|
||||
}" t-on-click="_onClickStar" title="Mark as Todo"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="props.hasReplyIcon">
|
||||
<span class="o_Message_command o_Message_commandReply o_Message_headerCommand fa fa-reply"
|
||||
t-att-class="{
|
||||
'o-message-selected': isSelected,
|
||||
'o-mobile': messaging.device.isMobile,
|
||||
}" t-on-click="_onClickReply" title="Reply"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="props.hasMarkAsReadIcon">
|
||||
<span class="o_Message_command o_Message_commandMarkAsRead o_Message_headerCommand fa fa-check"
|
||||
t-att-class="{
|
||||
'o-message-selected': isSelected,
|
||||
'o-mobile': messaging.device.isMobile,
|
||||
}" t-on-click="_onClickMarkAsRead" title="Mark as Read"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_Message_content" t-ref="content">
|
||||
<div class="o_Message_prettyBody" t-ref="prettyBody"/><!-- message.prettyBody is inserted here from _update() -->
|
||||
<t t-if="message.subtype_description and !message.isBodyEqualSubtypeDescription">
|
||||
<p t-esc="message.subtype_description"/>
|
||||
<div class="o_Message_contentWrapper position-relative" t-att-class="{ 'o_Message_contentWrapper_active': isActive and props.showActions }">
|
||||
<MessageActionList
|
||||
t-if="(isActive or message.actionList.showDeleteConfirm) and props.showActions"
|
||||
actionListLocalId="message.actionList.localId"
|
||||
hasReplyIcon="props.hasReplyIcon"
|
||||
hasMarkAsReadIcon="props.hasMarkAsReadIcon"
|
||||
/>
|
||||
<div class="o_Message_content m-2" t-ref="content">
|
||||
<div class="o_Message_prettyBody" t-ref="prettyBody"/><!-- message.prettyBody is inserted here from _update() -->
|
||||
<t t-if="message.subtype_description and !message.isBodyEqualSubtypeDescription">
|
||||
<p t-esc="message.subtype_description"/>
|
||||
</t>
|
||||
<t t-if="trackingValues.length > 0">
|
||||
<ul class="o_Message_trackingValues">
|
||||
<t t-foreach="trackingValues" t-as="value" t-key="value.id">
|
||||
<li>
|
||||
<div class="o_Message_trackingValue">
|
||||
<div class="o_Message_trackingValueFieldName o_Message_trackingValueItem" t-esc="value.changed_field"/>
|
||||
<t t-if="value.old_value">
|
||||
<div class="o_Message_trackingValueOldValue o_Message_trackingValueItem" t-esc="value.old_value"/>
|
||||
</t>
|
||||
<div class="o_Message_trackingValueSeparator o_Message_trackingValueItem fa fa-long-arrow-right" title="Changed" role="img"/>
|
||||
<t t-if="value.new_value">
|
||||
<div class="o_Message_trackingValueNewValue o_Message_trackingValueItem" t-esc="value.new_value"/>
|
||||
</t>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</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>
|
||||
<t t-if="trackingValues.length > 0">
|
||||
<ul class="o_Message_trackingValues">
|
||||
<t t-foreach="trackingValues" t-as="value" t-key="value.id">
|
||||
<li>
|
||||
<div class="o_Message_trackingValue">
|
||||
<div class="o_Message_trackingValueFieldName o_Message_trackingValueItem" t-esc="value.changed_field"/>
|
||||
<t t-if="value.old_value">
|
||||
<div class="o_Message_trackingValueOldValue o_Message_trackingValueItem" t-esc="value.old_value"/>
|
||||
</t>
|
||||
<div class="o_Message_trackingValueSeparator o_Message_trackingValueItem fa fa-long-arrow-right" title="Changed" role="img"/>
|
||||
<t t-if="value.new_value">
|
||||
<div class="o_Message_trackingValueNewValue o_Message_trackingValueItem" t-esc="value.new_value"/>
|
||||
</t>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<t t-if="message.attachments and message.attachments.length > 0">
|
||||
<div class="o_Message_footer">
|
||||
<AttachmentList
|
||||
class="o_Message_attachmentList"
|
||||
areAttachmentsDownloadable="true"
|
||||
areAttachmentsEditable="message.author === messaging.currentPartner"
|
||||
attachmentLocalIds="message.attachments.map(attachment => attachment.localId)"
|
||||
attachmentsDetailsMode="props.attachmentsDetailsMode"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="message.subject and !message.isSubjectSimilarToOriginThreadName">
|
||||
<p class="o_Message_subject mb-0">Subject: <t t-esc="message.subject"/></p>
|
||||
</t>
|
||||
<t t-if="message.attachments and message.attachments.length > 0">
|
||||
<div class="o_Message_footer">
|
||||
<AttachmentList
|
||||
class="o_Message_attachmentList"
|
||||
areAttachmentsDownloadable="true"
|
||||
areAttachmentsEditable="message.author === messaging.currentPartner"
|
||||
attachmentLocalIds="message.attachments.map(attachment => attachment.localId)"
|
||||
attachmentsDetailsMode="props.attachmentsDetailsMode"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -103,10 +103,11 @@ QUnit.test('basic rendering', async function (assert) {
|
||||
1,
|
||||
"message should display date"
|
||||
);
|
||||
await afterNextRender(() => messageEl.click());
|
||||
assert.strictEqual(
|
||||
messageEl.querySelectorAll(`:scope .o_Message_commands`).length,
|
||||
messageEl.querySelectorAll(`:scope .o_MessageActionList`).length,
|
||||
1,
|
||||
"message should display list of commands"
|
||||
"message should display list of actions"
|
||||
);
|
||||
assert.strictEqual(
|
||||
messageEl.querySelectorAll(`:scope .o_Message_content`).length,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/** @odoo-module */
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class MessageActionList extends Component {
|
||||
|
||||
/**
|
||||
* @returns {mail.message}
|
||||
*/
|
||||
get actionList() {
|
||||
return this.messaging && this.messaging.models['mail.message_action_list'].get(this.props.actionListLocalId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(MessageActionList, {
|
||||
props: {
|
||||
actionListLocalId: String,
|
||||
hasMarkAsReadIcon: Boolean,
|
||||
hasReplyIcon: Boolean,
|
||||
},
|
||||
template: "mail.MessageActionList",
|
||||
});
|
||||
|
||||
registerMessagingComponent(MessageActionList);
|
||||
@@ -0,0 +1,22 @@
|
||||
.o_MessageActionList {
|
||||
@include o-position-absolute($top: - map-get($spacers, 3), $right: map-get($spacers, 2));
|
||||
background-color: $white;
|
||||
border: $border-width solid $border-color;
|
||||
border-radius: $o-mail-rounded-rectangle-border-radius-sm;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px .5rem -.5rem black;
|
||||
}
|
||||
}
|
||||
|
||||
.o_MessageActionList_action {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: mix($border-color, $white);
|
||||
}
|
||||
}
|
||||
|
||||
.o_MessageActionList_actionStar.o_MessageActionList_actionStar_active {
|
||||
color: gold;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.MessageActionList" owl="1">
|
||||
<div class="o_MessageActionList" t-on-click="actionList.onClick">
|
||||
<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,
|
||||
'fa fa-lg fa-star-o': !actionList.message.isStarred,
|
||||
}" title="Mark as Todo" t-on-click="actionList.onClickToggleStar"/>
|
||||
<span t-if="props.hasReplyIcon" class="o_MessageActionList_action o_MessageActionList_actionReply p-2 fa fa-lg fa-reply" title="Reply" t-on-click="actionList.onClickReplyTo"/>
|
||||
<span t-if="props.hasMarkAsReadIcon" class="o_MessageActionList_action o_MessageActionList_actionMarkRead p-2 fa fa-lg fa-check" title="Mark as Read" t-on-click="actionList.onClickMarkAsRead"/>
|
||||
<span t-if="actionList.message.canBeDeleted" class="o_MessageActionList_action o_MessageActionList_actionDelete p-2 fa fa-lg fa-trash" title="Delete" t-on-click="actionList.onClickDelete"/>
|
||||
<DeleteMessageConfirmDialog t-if="actionList.showDeleteConfirm" actionListLocalId="actionList.localId"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -41,7 +41,11 @@ tour.register('mail_tour', {
|
||||
content: _t("Post your message on the thread"),
|
||||
position: "top",
|
||||
}, {
|
||||
trigger: '.o_Discuss_thread .o_Message_commandStar',
|
||||
trigger: '.o_Discuss_thread .o_Message',
|
||||
content: _t("Click on your message"),
|
||||
position: "top",
|
||||
}, {
|
||||
trigger: '.o_Discuss_thread .o_MessageActionList_actionStar',
|
||||
content: Markup(_t("Messages can be <b>starred</b> to remind you to check back later.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
|
||||
@@ -205,6 +205,11 @@ function factory(dependencies) {
|
||||
* @param {mail.message} message
|
||||
*/
|
||||
replyToMessage(message) {
|
||||
// When already replying to this message, just stop replying to it.
|
||||
if (this.replyingToMessage === message) {
|
||||
this.clearReplyingToMessage();
|
||||
return;
|
||||
}
|
||||
this.update({ replyingToMessage: link(message) });
|
||||
// avoid to reply to a note by a message and vice-versa.
|
||||
// subject to change later by allowing subtype choice.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerNewModel } from '@mail/model/model_core';
|
||||
import { attr, many2many, many2one, one2many } from '@mail/model/model_field';
|
||||
import { clear, insert, insertAndReplace, replace, unlinkAll } from '@mail/model/model_field_command';
|
||||
import { attr, many2many, many2one, one2many, one2one } from '@mail/model/model_field';
|
||||
import { clear, create, insert, insertAndReplace, replace, unlinkAll } from '@mail/model/model_field_command';
|
||||
import emojis from '@mail/js/emojis';
|
||||
import { addLink, htmlToTextContentInline, parseAndTransform, timeFromNow } from '@mail/js/utils';
|
||||
|
||||
import { session } from '@web/session';
|
||||
|
||||
import { str_to_datetime } from 'web.time';
|
||||
|
||||
function factory(dependencies) {
|
||||
@@ -250,6 +252,26 @@ function factory(dependencies) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the message's content.
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.body the new body of the message
|
||||
*/
|
||||
async updateContent({ body }) {
|
||||
const messageData = await this.env.services.rpc({
|
||||
route: '/mail/message/update_content',
|
||||
params: {
|
||||
body,
|
||||
message_id: this.id,
|
||||
},
|
||||
});
|
||||
if (!this.messaging) {
|
||||
return;
|
||||
}
|
||||
this.messaging.models['mail.message'].insert(messageData);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
@@ -261,6 +283,29 @@ function factory(dependencies) {
|
||||
return `${this.modelName}_${data.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeCanBeDeleted() {
|
||||
if (!session.is_admin && !this.isCurrentUserOrGuestAuthor) {
|
||||
return false;
|
||||
}
|
||||
if (!this.originThread) {
|
||||
return false;
|
||||
}
|
||||
if (this.originThread.model === 'mail.channel') {
|
||||
return this.message_type === 'comment';
|
||||
}
|
||||
return this.is_note;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeCanStarBeToggled() {
|
||||
return !this.isTemporary && !this.isTransient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
@@ -284,11 +329,15 @@ function factory(dependencies) {
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsCurrentPartnerAuthor() {
|
||||
_computeIsCurrentUserOrGuestAuthor() {
|
||||
return !!(
|
||||
this.author &&
|
||||
this.messaging.currentPartner &&
|
||||
this.messaging.currentPartner === this.author
|
||||
) || !!(
|
||||
this.guestAuthor &&
|
||||
this.messaging.currentGuest &&
|
||||
this.messaging.currentGuest === this.guestAuthor
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,6 +491,12 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
Message.fields = {
|
||||
actionList: one2one('mail.message_action_list', {
|
||||
default: create(),
|
||||
inverse: 'message',
|
||||
isCausal: true,
|
||||
readonly: true,
|
||||
}),
|
||||
attachments: many2many('mail.attachment', {
|
||||
inverse: 'messages',
|
||||
}),
|
||||
@@ -457,6 +512,18 @@ function factory(dependencies) {
|
||||
body: attr({
|
||||
default: "",
|
||||
}),
|
||||
/**
|
||||
* Whether this message can be deleted.
|
||||
*/
|
||||
canBeDeleted: attr({
|
||||
compute: '_computeCanBeDeleted',
|
||||
}),
|
||||
/**
|
||||
* Whether this message can be starred/unstarred.
|
||||
*/
|
||||
canStarBeToggled: attr({
|
||||
compute: '_computeCanStarBeToggled',
|
||||
}),
|
||||
/**
|
||||
* Determines the date of the message as a moment object.
|
||||
*/
|
||||
@@ -477,8 +544,8 @@ function factory(dependencies) {
|
||||
id: attr({
|
||||
required: true,
|
||||
}),
|
||||
isCurrentPartnerAuthor: attr({
|
||||
compute: '_computeIsCurrentPartnerAuthor',
|
||||
isCurrentUserOrGuestAuthor: attr({
|
||||
compute: '_computeIsCurrentUserOrGuestAuthor',
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerNewModel } from '@mail/model/model_core';
|
||||
import { attr, one2one } from '@mail/model/model_field';
|
||||
import { markEventHandled } from '@mail/utils/utils';
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
class MessageActionList extends dependencies['mail.model'] {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_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.onClickToggleStar = this.onClickToggleStar.bind(this);
|
||||
this.onDeleteConfirmDialogClosed = this.onDeleteConfirmDialogClosed.bind(this);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClick(ev) {
|
||||
markEventHandled(ev, 'MessageActionList.Click');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickConfirmDelete(ev) {
|
||||
this.message.updateContent({ body: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickDelete(ev) {
|
||||
this.update({ showDeleteConfirm: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickMarkAsRead(ev) {
|
||||
this.message.markAsRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the reply composer for this message (or closes it if it was
|
||||
* already opened).
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickReplyTo(ev) {
|
||||
markEventHandled(ev, 'MessageActionList.replyTo');
|
||||
this.message.replyTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickToggleStar(ev) {
|
||||
this.message.toggleStar();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
onDeleteConfirmDialogClosed(ev) {
|
||||
this.update({ showDeleteConfirm: false })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MessageActionList.fields = {
|
||||
message: one2one('mail.message', {
|
||||
inverse: 'actionList'
|
||||
}),
|
||||
/**
|
||||
* Whether to show the message delete-confirm dialog
|
||||
*/
|
||||
showDeleteConfirm: attr({
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
MessageActionList.modelName = 'mail.message_action_list';
|
||||
|
||||
return MessageActionList;
|
||||
}
|
||||
|
||||
registerNewModel('mail.message_action_list', factory);
|
||||
+2
@@ -89,6 +89,8 @@ function factory(dependencies) {
|
||||
return this._handleNotificationChannelRenamed(message.payload);
|
||||
case 'mail.channel_update':
|
||||
return this._handleNotificationChannelUpdate(message.payload);
|
||||
case 'mail.message_update':
|
||||
return this.messaging.models['mail.message'].insert(message.payload);
|
||||
case 'res.users_settings_changed':
|
||||
return this._handleNotificationResUsersSettings(message.payload);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user