[IMP] mail: allow user to edit message

task-2362167

closes odoo/odoo#76544

X-original-commit: 5d9d20748845652d2ecf527f4011522bf584807b
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Zelong Lin
2021-09-15 07:48:22 +00:00
committed by Sébastien Theys
parent 07ee514897
commit aa7bc757a2
16 changed files with 296 additions and 51 deletions
+3 -2
View File
@@ -144,15 +144,16 @@ class DiscussController(http.Controller):
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):
def mail_message_update_content(self, message_id, body, attachment_ids):
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)
message_sudo._update_content(body=body, attachment_ids=attachment_ids)
return {
'id': message_sudo.id,
'body': message_sudo.body,
'attachments': [('insert-and-replace', message_sudo.attachment_ids._attachment_format(commands=True))],
}
@http.route('/mail/attachment/upload', methods=['POST'], type='http', auth='public')
+22 -10
View File
@@ -36,14 +36,26 @@ class IrAttachment(models.Model):
except AccessError:
pass
def _attachment_format(self):
def _attachment_format(self, commands=False):
safari = request and request.httprequest.user_agent and request.httprequest.user_agent.browser == 'safari'
return [{
'checksum': attachment.checksum,
'id': attachment.id,
'filename': attachment.name,
'name': attachment.name,
'mimetype': 'application/octet-stream' if safari and attachment.mimetype and 'video' in attachment.mimetype else attachment.mimetype,
'res_id': attachment.res_id,
'res_model': attachment.res_model,
} for attachment in self]
res_list = []
for attachment in self:
res = {
'checksum': attachment.checksum,
'id': attachment.id,
'filename': attachment.name,
'name': attachment.name,
'mimetype': 'application/octet-stream' if safari and attachment.mimetype and 'video' in attachment.mimetype else attachment.mimetype,
}
if commands:
res['originThread'] = [('insert', {
'id': attachment.res_id,
'model': attachment.res_model,
})]
else:
res.update({
'res_id': attachment.res_id,
'res_model': attachment.res_model,
})
res_list.append(res)
return res_list
+1
View File
@@ -613,6 +613,7 @@ class Channel(models.Model):
'payload': {
'id': message.id,
'body': message.body,
'attachments': [('insert-and-replace', message.attachment_ids._attachment_format(commands=True))],
},
})
return super()._message_update_content_after_hook(message=message)
+11 -1
View File
@@ -674,11 +674,21 @@ class Message(models.Model):
return super(Message, self).export_data(fields_to_export)
def _update_content(self, body):
def _update_content(self, body, attachment_ids):
self.ensure_one()
thread = self.env[self.model].browse(self.res_id)
thread._check_can_update_message_content(self)
self.body = body
if not attachment_ids:
self.attachment_ids.unlink()
else:
message_values = {
'model': self.model,
'body': body,
'res_id': self.res_id,
}
attachement_values = thread._message_post_process_attachments([], attachment_ids, message_values)
self.update(attachement_values)
thread._message_update_content_after_hook(self)
# ------------------------------------------------------
@@ -118,6 +118,7 @@ export class Composer extends Component {
return (
this.props.hasThreadTyping ||
this.composer.attachments.length > 0 ||
this.composer.messageInEditing ||
!this.props.isCompact
);
}
@@ -168,6 +169,10 @@ export class Composer extends Component {
}
return;
}
if (this.composer.messageInEditing && this.composer.messageInEditing.isEditing) {
await this.composer.updateMessage();
return;
}
await this.composer.postMessage();
// TODO: we might need to remove trigger and use the store to wait for the post rpc to be done
// task-2252858
@@ -170,7 +170,9 @@
}
.o_Composer {
background-color: gray('100');
&:not(.o-messaging-in-editing) {
background-color: gray('100');
}
}
.o_Composer_actionButton.o-last.o-has-current-partner-avatar.o-composer-is-compact {
@@ -191,6 +193,10 @@
.o_Composer_buttons {
border: 0;
&.o-messaging-in-editing {
border-right: $border-width solid $border-color;
}
}
.o_Composer_coreMain:not(.o-composer-is-compact) {
@@ -9,6 +9,7 @@
'o-has-footer': hasFooter,
'o-has-header': hasHeader,
'o-is-compact': props.isCompact,
'o-messaging-in-editing': composer and composer.messageInEditing,
}"
t-on-keydown="_onKeydown"
>
@@ -24,7 +25,7 @@
attachmentLocalIds="composer.attachments.map(attachment => attachment.localId)"
isPending="true"
newAttachmentExtraData="newAttachmentExtraData"
threadLocalId="composer.thread.localId"
threadLocalId="composer.messageInEditing ? composer.messageInEditing.originThread.localId : composer.thread.localId"
t-ref="fileUploader"
/>
<t t-if="hasHeader">
@@ -34,6 +35,11 @@
on: <b><t t-esc="composer.thread.displayName"/></b>
</span>
</t>
<t t-if="props.hasThreadName and composer.messageInEditing and composer.messageInEditing.originThread">
<span class="o_Composer_threadName">
on: <b><t t-esc="composer.messageInEditing.originThread.displayName"/></b>
</span>
</t>
<t t-if="props.hasFollowers and !composer.isLog">
<!-- Text for followers -->
<small class="o_Composer_followers">
@@ -43,14 +49,17 @@
<t t-if="composer.thread and composer.thread.displayName">
&#32;&quot;<t t-esc="composer.thread.displayName"/>&quot;
</t>
<t t-elif="composer.messageInEditing and composer.messageInEditing.originThread.displayName">
&#32;&quot;<t t-esc="composer.messageInEditing.originThread.displayName"/>&quot;
</t>
<t t-else="">
this document
</t>
</b>
</small>
<ComposerSuggestedRecipientList
threadLocalId="composer.thread.localId"
/>
<t t-if="!composer.messageInEditing">
<ComposerSuggestedRecipientList threadLocalId="composer.thread.localId"/>
</t>
</t>
</div>
</t>
@@ -87,7 +96,7 @@
t-key="composer.localId"
t-ref="textInput"
/>
<div class="o_Composer_buttons" t-att-class="{ 'o-composer-is-compact': props.isCompact, 'o-mobile': messaging.device.isMobile }">
<div class="o_Composer_buttons" t-att-class="{ 'o-composer-is-compact': props.isCompact, 'o-mobile': messaging.device.isMobile, 'o-messaging-in-editing': composer and composer.messageInEditing }">
<div class="o_Composer_toolButtons"
t-att-class="{
'o-composer-has-current-partner-avatar': props.hasCurrentPartnerAvatar,
@@ -127,7 +136,7 @@
</div>
<t t-if="hasFooter">
<div class="o_Composer_coreFooter" t-att-class="{ 'o-composer-is-compact': props.isCompact }">
<t t-if="props.hasThreadTyping">
<t t-if="props.hasThreadTyping and !composer.messageInEditing">
<ThreadTextualTypingStatus class="o_Composer_threadTextualTypingStatus" threadLocalId="composer.thread.localId"/>
</t>
<t t-if="composer.attachments.length > 0">
@@ -140,6 +149,9 @@
<t t-if="!props.isCompact">
<t t-call="mail.Composer.actionButtons"/>
</t>
<t t-if="composer.messageInEditing">
<span class="text-muted">escape to <a href="#" t-on-click="composer.onClickCancelLink">cancel</a>, enter to <a href="#" t-on-click="composer.onClickSaveLink">save</a></span>
</t>
</div>
</t>
</t>
@@ -144,6 +144,10 @@ export class ComposerTextInput extends Component {
if (!this.composer) {
return;
}
if (this.composer.doFocus) {
this.focus();
this.composer.update({ doFocus: false });
}
if (this.composer.isLastStateChangeProgrammatic) {
this._textareaRef.el.value = this.composer.textInputContent;
if (this.composer.hasFocus) {
@@ -306,6 +310,10 @@ export class ComposerTextInput extends Component {
break;
case 'ArrowUp':
case 'PageUp':
if (ev.key === 'ArrowUp' && !this.composer.hasSuggestions && !this.composer.textInputContent && this.composer.thread) {
this.composer.thread.startEditingLastMessageFromCurrentUser();
break;
}
if (this.composer.hasSuggestions) {
this.composer.setPreviousSuggestionActive();
this.composer.update({ hasToScrollToActiveSuggestion: true });
@@ -313,6 +321,10 @@ export class ComposerTextInput extends Component {
break;
case 'ArrowDown':
case 'PageDown':
if (ev.key === 'ArrowDown' && !this.composer.hasSuggestions && !this.composer.textInputContent && this.composer.thread) {
this.composer.thread.startEditingLastMessageFromCurrentUser();
break;
}
if (this.composer.hasSuggestions) {
this.composer.setNextSuggestionActive();
this.composer.update({ hasToScrollToActiveSuggestion: true });
@@ -422,6 +422,9 @@ export class Message extends Component {
this._prettyBodyRef.el.innerHTML = this.message.prettyBody;
this._lastPrettyBody = this.message.prettyBody;
}
if (!this._prettyBodyRef.el) {
this._lastPrettyBody = undefined;
}
// Remove all readmore before if any before reinsert them with _insertReadMoreLess.
// This is needed because _insertReadMoreLess is working with direct DOM mutations
// which are not sync with Owl.
@@ -7,6 +7,7 @@
'o_Message_active': isActive,
'o-clicked': state.isClicked,
'o-discussion': message and (message.is_discussion or message.is_notification),
'o-editing': message.isEditing,
'o-has-message-selection': threadView and threadView.threadViewer and threadView.threadViewer.selectedMessage,
'o-highlighted': message and message.isHighlighted,
'o-mobile': messaging and messaging.device.isMobile,
@@ -117,7 +118,22 @@
<p class="o_Message_subject mx-2 mb-1">Subject: <t t-esc="message.subject"/></p>
</t>
<div class="o_Message_content mx-2" t-ref="content">
<div class="o_Message_prettyBody" t-ref="prettyBody"/><!-- message.prettyBody is inserted here from _update() -->
<t t-if="!message.isEditing or !message.composerInEditing">
<div class="o_Message_prettyBody" t-ref="prettyBody"/><!-- message.prettyBody is inserted here from _update() -->
</t>
<t t-if="message.isEditing and message.composerInEditing" >
<Composer
class="o_Message_composer mb-1"
composerLocalId="message.composerInEditing.localId"
hasCurrentPartnerAvatar="false"
hasDiscardButton="false"
hasMentionSuggestionsBelowPosition="false"
hasSendButton="false"
isCompact="true"
textInputSendShortcuts="['enter']"
t-ref="composer"
/>
</t>
<t t-if="message.subtype_description and !message.isBodyEqualSubtypeDescription">
<p t-esc="message.subtype_description"/>
</t>
@@ -14,6 +14,7 @@
}" 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_actionEdit p-2 fa fa-lg fa-pencil" title="Edit" t-on-click="actionList.onClickEdit"/>
<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>
@@ -63,7 +63,6 @@ function factory(dependencies) {
if ('name' in data) {
data2.name = data.name;
}
// relation
if ('res_id' in data && 'res_model' in data) {
data2.originThread = insert({
@@ -71,7 +70,9 @@ function factory(dependencies) {
model: data.res_model,
});
}
if ('originThread' in data) {
data2.originThread = data.originThread;
}
return data2;
}
@@ -378,7 +379,7 @@ function factory(dependencies) {
attachmentViewer: many2many('mail.attachment_viewer', {
inverse: 'attachments',
}),
checkSum: attr(),
checksum: attr(),
composers: many2many('mail.composer', {
inverse: 'attachments',
}),
@@ -408,6 +409,7 @@ function factory(dependencies) {
isImage: attr({
compute: '_computeIsImage',
}),
is_main: attr(),
isLinkedToComposer: attr({
compute: '_computeIsLinkedToComposer',
}),
@@ -33,6 +33,14 @@ function factory(dependencies) {
return res;
}
/**
* @override
*/
_created() {
this.onClickCancelLink = this.onClickCancelLink.bind(this);
this.onClickSaveLink = this.onClickSaveLink.bind(this);
}
/**
* @override
*/
@@ -59,6 +67,12 @@ function factory(dependencies) {
* currently used as a Discuss Inbox reply composer.
*/
discard() {
if (this.messageInEditing) {
this.messageInEditing.update({ isEditing: false });
if (this.messageInEditing.originThread && this.messageInEditing.originThread.composer) {
this.messageInEditing.originThread.composer.update({ doFocus: true });
}
}
if (this.discussAsReplying) {
this.discussAsReplying.clearReplyingToMessage();
}
@@ -153,8 +167,8 @@ function factory(dependencies) {
*/
_computeRecipients() {
const recipients = [...this.mentionedPartners];
if (this.thread && !this.isLog) {
for (const recipient of this.thread.suggestedRecipientInfoList) {
if (this.activeThread && !this.isLog) {
for (const recipient of this.activeThread.suggestedRecipientInfoList) {
if (recipient.partner && recipient.isSelected) {
recipients.push(recipient.partner);
}
@@ -163,6 +177,39 @@ function factory(dependencies) {
return replace(recipients);
}
/**
* Handles click on the cancel link.
*
* @param {MouseEvent} ev
*/
onClickCancelLink(ev) {
ev.preventDefault();
this.discard();
}
/**
* Handles click on the save link.
*
* @param {MouseEvent} ev
*/
onClickSaveLink(ev) {
ev.preventDefault();
if (!this.canPostMessage) {
if (this.hasUploadingAttachment) {
this.env.services['notification'].notify({
message: this.env._t("Please wait while the file is uploading."),
type: 'warning',
});
}
return;
}
if (this.messageInEditing && this.messageInEditing.isEditing) {
this.updateMessage();
return;
}
this.postMessage();
}
/**
* Open the full composer modal.
*/
@@ -173,9 +220,9 @@ function factory(dependencies) {
default_attachment_ids: attachmentIds,
default_body: escapeAndCompactTextContent(this.textInputContent),
default_is_log: this.isLog,
default_model: this.thread.model,
default_model: this.activeThread.model,
default_partner_ids: this.recipients.map(partner => partner.id),
default_res_id: this.thread.id,
default_res_id: this.activeThread.id,
mail_post_autofollow: true,
};
@@ -193,7 +240,7 @@ function factory(dependencies) {
return;
}
this._reset();
this.thread.loadNewMessages();
this.activeThread.loadNewMessages();
},
};
await this.env.bus.trigger('do-action', { action, options });
@@ -203,10 +250,10 @@ function factory(dependencies) {
* Post a message in provided composer's thread based on current composer fields values.
*/
async postMessage() {
if (this.thread.model === 'mail.channel') {
if (this.activeThread.model === 'mail.channel') {
const command = this._getCommandFromText(this.textInputContent);
if (command) {
await command.execute({ channel: this.thread, body: this.textInputContent });
await command.execute({ channel: this.activeThread, body: this.textInputContent });
if (this.exists()) {
this._reset();
}
@@ -214,7 +261,7 @@ function factory(dependencies) {
}
}
if (this.messaging.currentPartner) {
this.thread.unregisterCurrentPartnerIsTyping({ immediateNotify: true });
this.activeThread.unregisterCurrentPartnerIsTyping({ immediateNotify: true });
}
const escapedAndCompactContent = escapeAndCompactTextContent(this.textInputContent);
let body = escapedAndCompactContent.replace(/&nbsp;/g, ' ').trim();
@@ -235,12 +282,12 @@ function factory(dependencies) {
};
const params = {
'post_data': postData,
'thread_id': this.thread.id,
'thread_model': this.thread.model,
'thread_id': this.activeThread.id,
'thread_model': this.activeThread.model,
};
try {
this.update({ isPostingMessage: true });
if (this.thread.model === 'mail.channel') {
if (this.activeThread.model === 'mail.channel') {
Object.assign(postData, {
subtype_xmlid: 'mail.mt_comment',
});
@@ -275,13 +322,47 @@ function factory(dependencies) {
}
}
/**
* Update a posted message when the message is ready.
*/
async updateMessage() {
if (this.messageInEditing.originThread) {
if (this.messaging.currentPartner) {
this.messageInEditing.originThread.unregisterCurrentPartnerIsTyping({ immediateNotify: true });
}
}
const escapedAndCompactContent = escapeAndCompactTextContent(this.textInputContent);
let body = escapedAndCompactContent.replace(/&nbsp;/g, ' ').trim();
body = this._generateMentionsLinks(body);
body = parseAndTransform(body, addLink);
body = this._generateEmojisOnHtml(body);
let data = {
body: body,
attachment_ids: this.messageInEditing.attachments.concat(this.attachments).map(attachment => attachment.id),
};
try {
this.update({ isPostingMessage: true });
await this.messageInEditing.updateContent(data);
if (this.messageInEditing.originThread.model !== 'mail.channel') {
this.messageInEditing.originThread.refreshFollowers();
this.messageInEditing.originThread.fetchAndUpdateSuggestedRecipients();
}
this._reset();
} finally {
this.update({ isPostingMessage: false });
this.messageInEditing.update({ isEditing: false });
if (this.messageInEditing.originThread && this.messageInEditing.originThread.composer) {
this.messageInEditing.originThread.composer.update({ doFocus: true });
}
}
}
/**
* Called when current partner is inserting some input in composer.
* Useful to notify current partner is currently typing something in the
* composer of this thread to all other members.
*/
handleCurrentPartnerIsTyping() {
if (!this.thread) {
if (!this.activeThread) {
return;
}
if (!this.messaging.currentPartner) {
@@ -293,10 +374,10 @@ function factory(dependencies) {
) {
return;
}
if (this.thread.typingMembers.includes(this.messaging.currentPartner)) {
this.thread.refreshCurrentPartnerIsTyping();
if (this.activeThread.typingMembers.includes(this.messaging.currentPartner)) {
this.activeThread.refreshCurrentPartnerIsTyping();
} else {
this.thread.registerCurrentPartnerIsTyping();
this.activeThread.registerCurrentPartnerIsTyping();
}
}
@@ -385,6 +466,20 @@ function factory(dependencies) {
return link(firstRecord);
}
/**
* @private
* @returns {mail.thread}
*/
_computeActiveThread() {
if (this.messageInEditing && this.messageInEditing.originThread) {
return replace(this.messageInEditing.originThread);
}
if (this.thread) {
return replace(this.thread);
}
return clear();
}
/**
* @private
* @returns {boolean}
@@ -648,7 +743,7 @@ function factory(dependencies) {
return false;
}
if (command.channel_types) {
return command.channel_types.includes(this.thread.channel_type);
return command.channel_types.includes(this.activeThread.channel_type);
}
return true;
});
@@ -723,7 +818,7 @@ function factory(dependencies) {
}
const Model = this.messaging.models[this.suggestionModelName];
const searchTerm = this.suggestionSearchTerm;
await this.async(() => Model.fetchSuggestions(searchTerm, { thread: this.thread }));
await this.async(() => Model.fetchSuggestions(searchTerm, { thread: this.activeThread }));
this._updateSuggestionList();
if (
this.suggestionSearchTerm &&
@@ -774,8 +869,8 @@ function factory(dependencies) {
const [
mainSuggestedRecords,
extraSuggestedRecords = [],
] = Model.searchSuggestions(this.suggestionSearchTerm, { thread: this.thread });
const sortFunction = Model.getSuggestionSortFunction(this.suggestionSearchTerm, { thread: this.thread });
] = Model.searchSuggestions(this.suggestionSearchTerm, { thread: this.activeThread });
const sortFunction = Model.getSuggestionSortFunction(this.suggestionSearchTerm, { thread: this.activeThread });
mainSuggestedRecords.sort(sortFunction);
extraSuggestedRecords.sort(sortFunction);
// arbitrary limit to avoid displaying too many elements at once
@@ -801,6 +896,9 @@ function factory(dependencies) {
activeSuggestedRecord: many2one('mail.model', {
compute: '_computeActiveSuggestedRecord',
}),
activeThread: many2one('mail.thread', {
compute: '_computeActiveThread',
}),
attachments: many2many('mail.attachment', {
inverse: 'composers',
}),
@@ -826,6 +924,10 @@ function factory(dependencies) {
discussAsReplying: one2one('mail.discuss', {
inverse: 'replyingToMessageOriginThreadComposer',
}),
/**
* Determines whether this composer should be focused at next render.
*/
doFocus: attr(),
/**
* Determines the extra records that are currently suggested.
* Allows to have different model types of mentions through a dynamic
@@ -898,7 +1000,7 @@ function factory(dependencies) {
/**
* Determines the extra `mail.partner` (on top of existing followers)
* that will receive the message being composed by `this`, and that will
* also be added as follower of `this.thread`.
* also be added as follower of `this.activeThread`.
*/
recipients: many2many('mail.partner', {
compute: '_computeRecipients',
@@ -947,7 +1049,9 @@ function factory(dependencies) {
}),
thread: one2one('mail.thread', {
inverse: 'composer',
required: true,
}),
messageInEditing: one2one('mail.message', {
inverse: 'composerInEditing',
}),
};
Composer.onChanges = [
@@ -2,7 +2,7 @@
import { registerNewModel } from '@mail/model/model_core';
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 { clear, create, insert, insertAndReplace, link, replace, unlinkAll } from '@mail/model/model_field_command';
import emojis from '@mail/js/emojis';
import { addLink, htmlToTextContentInline, parseAndTransform, timeFromNow } from '@mail/js/utils';
@@ -276,6 +276,28 @@ function factory(dependencies) {
this.messaging.discuss.replyToMessage(this);
}
startEditing() {
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(this.body.replaceAll('<br>', '\n').replaceAll('</br>', '\n'), "text/html");
const textInputContent = htmlDoc.body.textContent;
const composerData = {
doFocus: true,
isLastStateChangeProgrammatic: true,
textInputCursorStart: textInputContent.length,
textInputCursorEnd: textInputContent.length,
textInputSelectionDirection: 'none',
textInputContent,
};
if (this.composerInEditing) {
this.composerInEditing.update(composerData);
} else {
this.messaging.models['mail.composer'].create({
messageInEditing: link(this),
...composerData,
});
}
this.update({ isEditing: true });
}
/**
* Toggle the starred status of the provided message.
*/
@@ -293,11 +315,12 @@ function factory(dependencies) {
* @param {Object} param0
* @param {string} param0.body the new body of the message
*/
async updateContent({ body }) {
async updateContent({ body, attachment_ids }) {
const messageData = await this.env.services.rpc({
route: '/mail/message/update_content',
params: {
body,
attachment_ids,
message_id: this.id,
},
});
@@ -569,6 +592,13 @@ function factory(dependencies) {
canStarBeToggled: attr({
compute: '_computeCanStarBeToggled',
}),
/**
* The composer in editing mode of a message.
*/
composerInEditing: one2one('mail.composer', {
inverse: 'messageInEditing',
isCausal: true,
}),
/**
* Determines the date of the message as a moment object.
*/
@@ -617,6 +647,12 @@ function factory(dependencies) {
compute: '_computeIsBodyEqualSubtypeDescription',
default: false,
}),
/*
* Determines whether the current user is currently editing this message.
*/
isEditing: attr({
default: false,
}),
/**
* Determine whether the message has to be considered empty or not.
*
@@ -16,6 +16,7 @@ function factory(dependencies) {
this.onClick = this.onClick.bind(this);
this.onClickConfirmDelete = this.onClickConfirmDelete.bind(this);
this.onClickDelete = this.onClickDelete.bind(this);
this.onClickEdit = this.onClickEdit.bind(this);
this.onClickMarkAsRead = this.onClickMarkAsRead.bind(this);
this.onReactionPopoverOpened = this.onReactionPopoverOpened.bind(this);
this.onReactionPopoverClosed = this.onReactionPopoverClosed.bind(this);
@@ -38,7 +39,10 @@ function factory(dependencies) {
* @param {MouseEvent} ev
*/
onClickConfirmDelete(ev) {
this.message.updateContent({ body: '' });
this.message.updateContent({
body: '',
attachment_ids: [],
});
}
/**
@@ -49,6 +53,14 @@ function factory(dependencies) {
this.update({ showDeleteConfirm: true });
}
/**
* @private
* @param {MouseEvent} ev
*/
onClickEdit(ev) {
this.message.startEditing();
}
/**
* @private
* @param {MouseEvent} ev
+15 -3
View File
@@ -702,9 +702,7 @@ function factory(dependencies) {
orderBy: [{ name: 'id', asc: false }],
}, { shadow: true }));
this.update({
originThreadAttachments: insertAndReplace(attachmentsData.map(data =>
this.messaging.models['mail.attachment'].convertData(data)
)),
originThreadAttachments: insertAndReplace(attachmentsData),
});
this.update({ areAttachmentsLoaded: true });
}
@@ -1189,6 +1187,20 @@ function factory(dependencies) {
});
}
/**
* Starts editing the last message of this thread from the current user.
*/
startEditingLastMessageFromCurrentUser() {
for (const threadView of this.threadViews) {
const messages = threadView.threadCache.orderedMessages;
messages.reverse();
const message = messages.find(message => message.isCurrentUserOrGuestAuthor && message.canBeDeleted);
if (message) {
message.startEditing();
}
}
}
/**
* Unfollow current partner from this thread.
*/