[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:
committed by
Sébastien Theys
parent
07ee514897
commit
aa7bc757a2
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
 "<t t-esc="composer.thread.displayName"/>"
|
||||
</t>
|
||||
<t t-elif="composer.messageInEditing and composer.messageInEditing.originThread.displayName">
|
||||
 "<t t-esc="composer.messageInEditing.originThread.displayName"/>"
|
||||
</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(/ /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(/ /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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user