diff options
Diffstat (limited to 'addons/mail/static/src/components/message/message.js')
| -rw-r--r-- | addons/mail/static/src/components/message/message.js | 621 |
1 files changed, 621 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/message/message.js b/addons/mail/static/src/components/message/message.js new file mode 100644 index 00000000000..f4940a1e566 --- /dev/null +++ b/addons/mail/static/src/components/message/message.js @@ -0,0 +1,621 @@ +odoo.define('mail/static/src/components/message/message.js', function (require) { +'use strict'; + +const components = { + AttachmentList: require('mail/static/src/components/attachment_list/attachment_list.js'), + MessageSeenIndicator: require('mail/static/src/components/message_seen_indicator/message_seen_indicator.js'), + ModerationBanDialog: require('mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js'), + ModerationDiscardDialog: require('mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js'), + ModerationRejectDialog: require('mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js'), + NotificationPopover: require('mail/static/src/components/notification_popover/notification_popover.js'), + PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'), +}; +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { timeFromNow } = require('mail.utils'); + +const { _lt } = require('web.core'); +const { getLangDatetimeFormat } = require('web.time'); + +const { Component, useState } = owl; +const { useRef } = owl.hooks; + +const READ_MORE = _lt("read more"); +const READ_LESS = _lt("read less"); + +class Message extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + this.state = useState({ + // Determine if the moderation ban dialog is displayed. + hasModerationBanDialog: false, + // Determine if the moderation discard dialog is displayed. + hasModerationDiscardDialog: false, + // Determine if the moderation reject dialog is displayed. + hasModerationRejectDialog: false, + /** + * Determine whether the message is clicked. When message is in + * clicked state, it keeps displaying the commands. + */ + isClicked: false, + /** + * Time elapsed from message datetime to current datetime. + */ + timeElapsed: null, + }); + useStore(props => { + const message = this.env.models['mail.message'].get(props.messageLocalId); + const author = message ? message.author : undefined; + const partnerRoot = this.env.messaging.partnerRoot; + const originThread = message ? message.originThread : undefined; + const threadViewer = this.env.models['mail.thread_viewer'].get(props.threadViewerLocalId); + const thread = threadViewer ? threadViewer.thread : undefined; + const threadStringifiedDomain = threadViewer + ? threadViewer.stringifiedDomain + : undefined; + return { + attachments: message + ? message.attachments.map(attachment => attachment.__state) + : undefined, + author: author ? author.__state : undefined, + hasMessageCheckbox: message ? message.hasCheckbox : false, + isDeviceMobile: this.env.messaging.device.isMobile, + isMessageChecked: message && threadViewer + ? message.isChecked(thread, threadStringifiedDomain) + : false, + message: message ? message.__state : undefined, + notifications: message ? message.notifications.map(notif => notif.__state) : [], + originThread: originThread ? originThread.__state : undefined, + partnerRoot: partnerRoot ? partnerRoot.__state : undefined, + thread: thread ? thread.__state : undefined, + threadViewer: threadViewer ? threadViewer.__state : undefined, + }; + }, { + compareDepth: { + notifications: 1, + }, + }); + /** + * Reference to the content of the message. + */ + this._contentRef = useRef('content'); + /** + * To get checkbox state. + */ + this._checkboxRef = useRef('checkbox'); + /** + * Id of setInterval used to auto-update time elapsed of message at + * regular time. + */ + this._intervalId = undefined; + this._constructor(); + } + + /** + * Allows patching constructor. + */ + _constructor() {} + + mounted() { + this._insertReadMoreLess($(this._contentRef.el)); + this._update(); + } + + patched() { + this._update(); + } + + willUnmount() { + clearInterval(this._intervalId); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {string} + */ + get avatar() { + if ( + this.message.author && + this.message.author === this.env.messaging.partnerRoot + ) { + return '/mail/static/src/img/odoobot.png'; + } else if (this.message.author) { + // TODO FIXME for public user this might not be accessible. task-2223236 + // we should probably use the correspondig attachment id + access token + // or create a dedicated route to get message image, checking the access right of the message + return `/web/image/res.partner/${this.message.author.id}/image_128`; + } else if (this.message.message_type === 'email') { + return '/mail/static/src/img/email_icon.png'; + } + return '/mail/static/src/img/smiley/avatar.jpg'; + } + + /** + * Get the date time of the message at current user locale time. + * + * @returns {string} + */ + get datetime() { + return this.message.date.format(getLangDatetimeFormat()); + } + + /** + * Determine whether author redirect feature is enabled on message. + * Click on message author should redirect to author. + * + * @returns {boolean} + */ + get hasAuthorRedirect() { + if (!this.props.hasAuthorRedirect) { + return false; + } + if (!this.message.author) { + return false; + } + if (this.message.author === this.env.messaging.currentPartner) { + return false; + } + return true; + } + + /** + * Determine whether the message origin thread is the same as the context + * of displaying this message. In other word, if the enclosing thread + * component of this message component is linked to the origin thread of + * this message, then the origin is the same. + * + * @returns {boolean} + */ + get hasDifferentOriginThread() { + if (!this.threadViewer) { + return false; + } + if (!this.message.originThread) { + return false; + } + return this.message.originThread !== this.threadViewer.thread; + } + + /** + * @returns {mail.attachment[]} + */ + get imageAttachments() { + return this.message.attachments.filter(attachment => attachment.fileType === 'image'); + } + + /** + * Tell whether the bottom of this message is visible or not. + * + * @param {Object} param0 + * @param {integer} [offset=0] + * @returns {boolean} + */ + isBottomVisible({ offset=0 } = {}) { + if (!this.el) { + return false; + } + const elRect = this.el.getBoundingClientRect(); + if (!this.el.parentNode) { + return false; + } + const parentRect = this.el.parentNode.getBoundingClientRect(); + // bottom with (double) 10px offset + return ( + elRect.bottom < parentRect.bottom + offset && + parentRect.top < elRect.bottom + offset + ); + } + + /** + * Tell whether the message is partially visible on browser window or not. + * + * @returns {boolean} + */ + isPartiallyVisible() { + const elRect = this.el.getBoundingClientRect(); + if (!this.el.parentNode) { + return false; + } + const parentRect = this.el.parentNode.getBoundingClientRect(); + // intersection with 5px offset + return ( + elRect.top < parentRect.bottom + 5 && + parentRect.top < elRect.bottom + 5 + ); + } + + /** + * Determine whether the message is starred. + * + * @returns {boolean} + */ + get isStarred() { + return this.message.allThreads.includes(this.env.messaging.starred); + } + + /** + * @returns {mail.message} + */ + get message() { + return this.env.models['mail.message'].get(this.props.messageLocalId); + } + + /** + * @returns {mail.attachment[]} + */ + get nonImageAttachments() { + return this.message.attachments.filter(attachment => attachment.fileType !== 'image'); + } + + /** + * Make this message viewable in its enclosing scroll environment (usually + * message list). + * + * @param {Object} [param0={}] + * @param {string} [param0.behavior='auto'] + * @param {string} [param0.block='end'] + * @returns {Promise} + */ + async scrollIntoView({ behavior='auto', block='end' }={}) { + this.el.scrollIntoView({ + behavior, + block, + inline: 'nearest', + }); + if (behavior === 'smooth') { + return new Promise(resolve => setTimeout(resolve, 500)); + } else { + return Promise.resolve(); + } + } + + /** + * Get the shorttime format of the message date. + * + * @returns {string} + */ + get shortTime() { + return this.message.date.format('hh:mm'); + } + + /** + * @returns {mail.thread_viewer} + */ + get threadViewer() { + return this.env.models['mail.thread_viewer'].get(this.props.threadViewerLocalId); + } + + /** + * @returns {Object} + */ + get trackingValues() { + return this.message.tracking_value_ids.map(trackingValue => { + const value = Object.assign({}, trackingValue); + value.changed_field = _.str.sprintf(this.env._t("%s:"), value.changed_field); + if (value.field_type === 'datetime') { + if (value.old_value) { + value.old_value = + moment.utc(value.old_value).local().format('LLL'); + } + if (value.new_value) { + value.new_value = + moment.utc(value.new_value).local().format('LLL'); + } + } else if (value.field_type === 'date') { + if (value.old_value) { + value.old_value = + moment(value.old_value).local().format('LL'); + } + if (value.new_value) { + value.new_value = + moment(value.new_value).local().format('LL'); + } + } + return value; + }); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Modifies the message to add the 'read more/read less' functionality + * All element nodes with 'data-o-mail-quote' attribute are concerned. + * All text nodes after a ``#stopSpelling`` element are concerned. + * Those text nodes need to be wrapped in a span (toggle functionality). + * All consecutive elements are joined in one 'read more/read less'. + * + * @private + * @param {jQuery} $element + */ + _insertReadMoreLess($element) { + const groups = []; + let readMoreNodes; + + // nodeType 1: element_node + // nodeType 3: text_node + const $children = $element.contents() + .filter((index, content) => + content.nodeType === 1 || (content.nodeType === 3 && content.nodeValue.trim()) + ); + + for (const child of $children) { + let $child = $(child); + + // Hide Text nodes if "stopSpelling" + if ( + child.nodeType === 3 && + $child.prevAll('[id*="stopSpelling"]').length > 0 + ) { + // Convert Text nodes to Element nodes + $child = $('<span>', { + text: child.textContent, + 'data-o-mail-quote': '1', + }); + child.parentNode.replaceChild($child[0], child); + } + + // Create array for each 'read more' with nodes to toggle + if ( + $child.attr('data-o-mail-quote') || + ( + $child.get(0).nodeName === 'BR' && + $child.prev('[data-o-mail-quote="1"]').length > 0 + ) + ) { + if (!readMoreNodes) { + readMoreNodes = []; + groups.push(readMoreNodes); + } + $child.hide(); + readMoreNodes.push($child); + } else { + readMoreNodes = undefined; + this._insertReadMoreLess($child); + } + } + + for (const group of groups) { + // Insert link just before the first node + const $readMoreLess = $('<a>', { + class: 'o_Message_readMore', + href: '#', + text: READ_MORE, + }).insertBefore(group[0]); + + // Toggle All next nodes + let isReadMore = true; + $readMoreLess.click(e => { + e.preventDefault(); + isReadMore = !isReadMore; + for (const $child of group) { + $child.hide(); + $child.toggle(!isReadMore); + } + $readMoreLess.text(isReadMore ? READ_MORE : READ_LESS); + }); + } + } + + /** + * @private + */ + _update() { + if (!this.state.timeElapsed) { + this.state.timeElapsed = timeFromNow(this.message.date); + } + clearInterval(this._intervalId); + this._intervalId = setInterval(() => { + this.state.timeElapsed = timeFromNow(this.message.date); + }, 60 * 1000); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onChangeCheckbox() { + this.message.toggleCheck(this.threadViewer.thread, this.threadViewer.stringifiedDomain); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClick(ev) { + if (ev.target.closest('.o_mention')) { + this.env.messaging.redirect({ + id: Number(ev.target.dataset.oeId), + model: ev.target.dataset.oeModel, + }); + ev.preventDefault(); + return; + } + if (ev.target.closest('.o_mail_redirect')) { + this.env.messaging.redirect({ + id: Number(ev.target.dataset.oeId), + model: ev.target.dataset.oeModel, + }); + ev.preventDefault(); + return; + } + ev.stopPropagation(); + this.state.isClicked = !this.state.isClicked; + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickAuthor(ev) { + if (!this.hasAuthorRedirect) { + return; + } + if (!this.message.author) { + return; + } + this.env.messaging.redirect({ + id: this.message.author.id, + model: this.message.author.model, + }); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickFailure(ev) { + this.message.openResendAction(); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickModerationAccept(ev) { + ev.preventDefault(); + this.message.moderate('accept'); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickModerationAllow(ev) { + ev.preventDefault(); + this.message.moderate('allow'); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickModerationBan(ev) { + ev.preventDefault(); + this.state.hasModerationBanDialog = true; + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickModerationDiscard(ev) { + ev.preventDefault(); + this.state.hasModerationDiscardDialog = true; + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickModerationReject(ev) { + ev.preventDefault(); + this.state.hasModerationRejectDialog = true; + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickOriginThread(ev) { + ev.preventDefault(); + this.env.messaging.redirect({ + id: this.message.originThread.id, + model: this.message.originThread.model, + }); + } + + /** + * @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) { + ev.stopPropagation(); + this.message.replyTo(); + } + + /** + * @private + */ + _onDialogClosedModerationBan() { + this.state.hasModerationBanDialog = false; + } + + /** + * @private + */ + _onDialogClosedModerationDiscard() { + this.state.hasModerationDiscardDialog = false; + } + + /** + * @private + */ + _onDialogClosedModerationReject() { + this.state.hasModerationRejectDialog = false; + } + +} + +Object.assign(Message, { + components, + defaultProps: { + hasAuthorRedirect: false, + hasCheckbox: false, + hasMarkAsReadIcon: false, + hasReplyIcon: false, + isSelected: false, + isSquashed: false, + }, + props: { + attachmentsDetailsMode: { + type: String, + optional: true, + validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop), + }, + hasAuthorRedirect: Boolean, + hasCheckbox: Boolean, + hasMarkAsReadIcon: Boolean, + hasReplyIcon: Boolean, + isSelected: Boolean, + isSquashed: Boolean, + messageLocalId: String, + threadViewerLocalId: { + type: String, + optional: true, + }, + }, + template: 'mail.Message', +}); + +return Message; + +}); |
