[FIX] mail: fix (inbox reply) composer event handlers (and discard)

task-2092950

closes odoo/odoo#53731

X-original-commit: c29f5776594c4d2278ce163a78d8eb917fb672c0
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2020-06-26 11:21:35 +00:00
parent dc011d1246
commit 1c058b72d8
17 changed files with 578 additions and 68 deletions
@@ -7,6 +7,7 @@ const components = {
ThreadViewer: require('mail/static/src/components/thread_viewer/thread_viewer.js'),
};
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { isEventHandled } = require('mail/static/src/utils/utils.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -250,6 +251,10 @@ class ChatWindow extends Component {
*/
_onFocusinThread(ev) {
ev.stopPropagation();
if (!this.chatWindow) {
// prevent crash on destroy
return;
}
this.chatWindow.update({ isFocused: true });
}
@@ -293,6 +298,12 @@ class ChatWindow extends Component {
}
break;
case 'Escape':
if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
break;
}
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
break;
}
ev.preventDefault();
this.chatWindow.close();
break;
@@ -527,8 +527,13 @@ QUnit.test('chat window: open / close', async function (assert) {
});
QUnit.test('chat window: close on ESCAPE', async function (assert) {
assert.expect(4);
assert.expect(12);
this.data['res.partner'].records = [{
email: "testpartnert@odoo.com",
id: 11,
name: "TestPartner",
}];
Object.assign(this.data.initMessaging, {
channel_slots: {
channel_channel: [{
@@ -569,12 +574,73 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click()
);
assert.verifySteps(['rpc:channel_fold/open']);
assert.containsOnce(
document.body,
'.o_ChatWindow',
"chat window should be opened after click on preview"
);
await afterNextRender(() =>
document.querySelector(`.o_Composer_buttonEmojis`).click()
);
assert.containsOnce(
document.body,
'.o_EmojisPopover',
"emojis popover should be opened after click on emojis button"
);
await afterNextRender(() => {
document.querySelector(`.o_ChatWindow`).focus();
const kevt = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ChatWindow`).dispatchEvent(kevt);
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_EmojisPopover',
"emojis popover should be closed after pressing escape on emojis button"
);
assert.containsOnce(
document.body,
'.o_ChatWindow',
"chat window should still be opened after pressing escape on emojis button"
);
await afterNextRender(() => {
document.execCommand('insertText', false, "@");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
});
assert.containsOnce(
document.body,
'.o_ComposerTextInput_mentionDropdownPropositionList',
"mention suggestion should be opened after typing @"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_ComposerTextInput_mentionDropdownPropositionList',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
document.body,
'.o_ChatWindow',
"chat window should still be opened after pressing escape on mention suggestion"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_ChatWindow',
"chat window should be closed after pressing escape if there was no other priority escape handler"
);
assert.verifySteps(['rpc:channel_fold/closed']);
});
@@ -11,6 +11,10 @@ const components = {
};
const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const {
isEventHandled,
markEventHandled,
} = require('mail/static/src/utils/utils.js');
const mailUtils = require('mail.utils');
const { Component } = owl;
@@ -35,8 +39,8 @@ class Composer extends Component {
};
});
/**
* Reference of the emoji button. Useful to include emoji popover as
* click "inside" the composer for the prop `isDiscardOnClickAway`.
* Reference of the emoji popover. Useful to include emoji popover as
* contained "inside" the composer.
*/
this._emojisPopoverRef = useRef('emojisPopover');
/**
@@ -48,6 +52,7 @@ class Composer extends Component {
* Reference of the text input component.
*/
this._textInputRef = useRef('textInput');
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
}
@@ -70,6 +75,22 @@ class Composer extends Component {
return this.env.models['mail.composer'].get(this.props.composerLocalId);
}
/**
* Returns whether the given node is self or a children of self, including
* the emoji popover.
*
* @param {Node} node
* @returns {boolean}
*/
contains(node) {
// emoji popover is outside but should be considered inside
const emojisPopover = this._emojisPopoverRef.comp;
if (emojisPopover && emojisPopover.contains(node)) {
return true;
}
return this.el.contains(node);
}
/**
* Get the current partner image URL.
*
@@ -172,23 +193,13 @@ class Composer extends Component {
}
/**
* Discards the composer when clicking away from the Inbox reply in discuss.
* TODO maybe move this in discuss. task-2092950
* Discards the composer when clicking away.
*
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
// in discuss app: prevents discarding the composer when clicking away
if (!this.props.isDiscardOnClickAway) {
return;
}
// prevents discarding when clicking on self (on discuss and not on discuss)
if (this.el.contains(ev.target)) {
return;
}
// emoji popover is outside but should be considered inside
if (this._emojisPopoverRef.comp.isInsideEventTarget(ev.target)) {
if (this.contains(ev.target)) {
return;
}
this.composer.discard();
@@ -280,6 +291,37 @@ class Composer extends Component {
this.composer.focus();
}
/**
* @private
* @param {KeyboardEvent} ev
*/
_onKeydown(ev) {
if (ev.key === 'Escape') {
if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
return;
}
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
return;
}
ev.preventDefault();
this.composer.discard();
}
}
/**
* @private
* @param {KeyboardEvent} ev
*/
_onKeydownEmojiButton(ev) {
if (ev.key === 'Escape') {
if (this._emojisPopoverRef.comp) {
this._emojisPopoverRef.comp.close();
this.composer.focus();
markEventHandled(ev, 'Composer.closeEmojisPopover');
}
}
}
/**
* @private
* @param {CustomEvent} ev
@@ -315,7 +357,6 @@ Object.assign(Composer, {
hasThreadName: false,
hasThreadTyping: false,
isCompact: true,
isDiscardOnClickAway: false,
isExpandable: false,
},
props: {
@@ -357,7 +398,6 @@ Object.assign(Composer, {
optional: true,
},
isCompact: Boolean,
isDiscardOnClickAway: Boolean,
isExpandable: Boolean,
},
template: 'mail.Composer',
@@ -10,6 +10,7 @@
'o-has-header': hasHeader,
'o-is-compact': props.isCompact,
}"
t-on-keydown="_onKeydown"
>
<t t-if="composer">
<t t-if="isDropZoneVisible.value">
@@ -77,18 +78,19 @@
<div class="o_Composer_toolButtonSeparator"/>
</t>
<div class="o_Composer_primaryToolButtons" t-att-class="{ 'o-composer-is-compact': props.isCompact }">
<Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection" t-ref="emojisPopover">
<Popover position="'top'" t-on-o-emoji-selection="_onEmojiSelection">
<!-- TODO FIXME o-open not possible to code due to https://github.com/odoo/owl/issues/693 -->
<button class="o_Composer_button o_Composer_buttonEmojis o_Composer_toolButton btn btn-light"
t-att-class="{
'o-open': false and state.displayed,
'o-mobile': env.messaging.device.isMobile,
}"
t-on-keydown="_onKeydownEmojiButton"
>
<i class="fa fa-smile-o"/>
</button>
<t t-set="opened">
<EmojisPopover/>
<EmojisPopover t-ref="emojisPopover"/>
</t>
</Popover>
<button class="o_Composer_button o_Composer_buttonAttachment o_Composer_toolButton btn btn-light fa fa-paperclip" t-att-class="{ 'o-mobile': env.messaging.device.isMobile }" title="Add attachment" type="button" t-on-click="_onClickAddAttachment"/>
@@ -266,7 +266,7 @@ QUnit.test('display partner mention suggestions on typing "@"', async function (
this.data['res.users'].records = [{
partner_id: 11,
}];
await this.start({});
await this.start();
const composer = this.env.models['mail.composer'].create();
await this.createComposerComponent(composer);
@@ -6,6 +6,7 @@ const useStore = require('mail/static/src/component_hooks/use_store/use_store.js
const components = {
PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'),
};
const { markEventHandled } = require('mail/static/src/utils/utils.js');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -206,6 +207,13 @@ class ComposerTextInput extends Component {
*/
_onKeydownTextarea(ev) {
switch (ev.key) {
case 'Escape':
if (this.composer.hasSuggestedPartners) {
ev.preventDefault();
this.composer.closeMentionSuggestions();
markEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions');
}
break;
// UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions
case 'ArrowUp':
case 'PageUp':
@@ -256,9 +264,8 @@ class ComposerTextInput extends Component {
*/
_onKeyupTextarea(ev) {
switch (ev.key) {
// ESCAPE: close mention suggestions
case 'Escape':
this._onKeyupTextareaEscape(ev);
// already handled in _onKeydownTextarea, break to avoid default
break;
// ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
case 'Enter':
@@ -308,21 +315,6 @@ class ComposerTextInput extends Component {
}
}
/**
* @private
* @param {KeyboardEvent} ev
*/
_onKeyupTextareaEscape(ev) {
if (this.composer.hasSuggestedPartners) {
this.composer.closeMentionSuggestions();
} else {
if (!this._isEmpty()) {
return;
}
this.composer.discard();
}
}
/**
* @private
* @param {Event} ev
@@ -96,7 +96,6 @@
hasCurrentPartnerAvatar="!env.messaging.device.isMobile"
hasDiscardButton="true"
hasThreadName="true"
isDiscardOnClickAway="true"
t-on-o-message-posted="_onReplyingToMessageMessagePosted"
/>
</t>
@@ -0,0 +1,300 @@
odoo.define('mail/static/src/components/discuss/tests/discuss_inbox_tests.js', function (require) {
'use strict';
const {
afterEach: utilsAfterEach,
afterNextRender,
beforeEach: utilsBeforeEach,
nextAnimationFrame,
start: utilsStart,
} = require('mail/static/src/utils/test_utils.js');
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
QUnit.module('discuss', {}, function () {
QUnit.module('discuss_inbox_tests.js', {
beforeEach() {
utilsBeforeEach(this);
this.start = async params => {
let { env, widget } = await utilsStart(Object.assign({}, params, {
autoOpenDiscuss: true,
data: this.data,
hasDiscuss: true,
}));
this.env = env;
this.widget = widget;
};
},
afterEach() {
if (this.widget) {
this.widget.destroy();
}
utilsAfterEach(this);
},
});
QUnit.test('reply: discard on pressing escape', async function (assert) {
assert.expect(9);
this.data['res.partner'].records = [{
email: "testpartnert@odoo.com",
id: 11,
name: "TestPartner",
}];
this.data['mail.message'].records = [{
author_id: [7, "Demo"],
body: "<p>Test</p>",
date: "2019-04-20 11:00:00",
id: 100,
message_type: 'comment',
needaction: true,
needaction_partner_ids: [3],
model: 'project.task',
record_name: 'Refactoring',
res_id: 20,
}];
await this.start();
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await afterNextRender(() =>
document.querySelector('.o_Message_commandReply').click()
);
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
await afterNextRender(() =>
document.querySelector(`.o_Composer_buttonEmojis`).click()
);
assert.containsOnce(
document.body,
'.o_EmojisPopover',
"emojis popover should be opened after click on emojis button"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_EmojisPopover',
"emojis popover should be closed after pressing escape on emojis button"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be opened after pressing escape on emojis button"
);
await afterNextRender(() => {
document.execCommand('insertText', false, "@");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
});
assert.containsOnce(
document.body,
'.o_ComposerTextInput_mentionDropdownPropositionList',
"mention suggestion should be opened after typing @"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_ComposerTextInput_mentionDropdownPropositionList',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be opened after pressing escape on mention suggestion"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after pressing escape if there was no other priority escape handler"
);
});
QUnit.test('reply: discard on discard button click', async function (assert) {
assert.expect(4);
this.data['mail.message'].records = [{
author_id: [7, "Demo"],
body: "<p>Test</p>",
date: "2019-04-20 11:00:00",
id: 100,
message_type: 'comment',
needaction: true,
needaction_partner_ids: [3],
model: 'project.task',
record_name: 'Refactoring',
res_id: 20,
}];
await this.start();
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await afterNextRender(() =>
document.querySelector('.o_Message_commandReply').click()
);
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
assert.containsOnce(
document.body,
'.o_Composer_buttonDiscard',
"composer should have a discard button"
);
await afterNextRender(() =>
document.querySelector(`.o_Composer_buttonDiscard`).click()
);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking on discard"
);
});
QUnit.test('reply: discard on reply button toggle', async function (assert) {
assert.expect(3);
this.data['mail.message'].records = [{
author_id: [7, "Demo"],
body: "<p>Test</p>",
date: "2019-04-20 11:00:00",
id: 100,
message_type: 'comment',
needaction: true,
needaction_partner_ids: [3],
model: 'project.task',
record_name: 'Refactoring',
res_id: 20,
}];
await this.start();
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await afterNextRender(() =>
document.querySelector('.o_Message_commandReply').click()
);
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
await afterNextRender(() =>
document.querySelector(`.o_Message_commandReply`).click()
);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking on reply button again"
);
});
QUnit.test('reply: discard on click away', async function (assert) {
assert.expect(7);
this.data['mail.message'].records = [{
author_id: [7, "Demo"],
body: "<p>Test</p>",
date: "2019-04-20 11:00:00",
id: 100,
message_type: 'comment',
needaction: true,
needaction_partner_ids: [3],
model: 'project.task',
record_name: 'Refactoring',
res_id: 20,
}];
await this.start();
assert.containsOnce(
document.body,
'.o_Message',
"should display a single message"
);
await afterNextRender(() =>
document.querySelector('.o_Message_commandReply').click()
);
assert.containsOnce(
document.body,
'.o_Composer',
"should have composer after clicking on reply to message"
);
document.querySelector(`.o_ComposerTextInput_textarea`).click();
await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be there after clicking inside itself"
);
await afterNextRender(() =>
document.querySelector(`.o_Composer_buttonEmojis`).click()
);
assert.containsOnce(
document.body,
'.o_EmojisPopover',
"emojis popover should be opened after clicking on emojis button"
);
await afterNextRender(() => {
document.querySelector(`.o_EmojisPopover_emoji`).click();
});
assert.containsNone(
document.body,
'.o_EmojisPopover',
"emojis popover should be closed after selecting an emoji"
);
assert.containsOnce(
document.body,
'.o_Composer',
"reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)"
);
await afterNextRender(() =>
document.querySelector(`.o_Message`).click()
);
assert.containsNone(
document.body,
'.o_Composer',
"reply composer should be closed after clicking away"
);
});
});
});
});
});
@@ -15,6 +15,27 @@ class EmojisPopover extends Component {
this.emojis = emojis;
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
close() {
this.trigger('o-popover-close');
}
/**
* Returns whether the given node is self or a children of self.
*
* @param {Node} node
* @returns {boolean}
*/
contains(node) {
if (!this.el) {
return false;
}
return this.el.contains(node);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -24,7 +45,7 @@ class EmojisPopover extends Component {
* @param {MouseEvent} ev
*/
_onClickEmoji(ev) {
this.trigger('o-popover-close');
this.close();
this.trigger('o-emoji-selection', {
unicode: ev.currentTarget.dataset.unicode,
});
@@ -78,6 +78,10 @@ class Message extends Component {
notifications: 1,
},
});
/**
* The intent of the reply button depends on the last rendered state.
*/
this._wasSelected;
/**
* Reference to the content of the message.
*/
@@ -399,6 +403,7 @@ class Message extends Component {
* @private
*/
_update() {
this._wasSelected = this.props.isSelected;
if (!this.state.timeElapsed) {
this.state.timeElapsed = timeFromNow(this.message.date);
}
@@ -549,8 +554,15 @@ class Message extends Component {
* @param {MouseEvent} ev
*/
_onClickReply(ev) {
ev.stopPropagation();
this.message.replyTo();
// 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.env.messaging.discuss.clearReplyingToMessage();
} else {
this.message.replyTo();
}
}
/**
@@ -89,7 +89,7 @@ class ThreadViewer extends Component {
*
* @private
*/
async _update() {
_update() {
const messageList = this._messageListRef.comp;
this.trigger('o-rendered');
/**
@@ -294,7 +294,7 @@ function factory(dependencies) {
* chat window to put initially on mount.
*/
threadInitialScrollTop: attr(),
thread: one2one('mail.thread', {
thread: many2one('mail.thread', {
related: 'threadViewer.thread',
}),
threadDisplayName: attr({
@@ -27,17 +27,14 @@ function factory(dependencies) {
});
}
/**
* Hides the composer, which only makes sense if the composer is
* currently used as a Discuss Inbox reply composer.
*/
discard() {
const discuss = this.env.messaging.discuss;
const thread = this.thread;
if (
!discuss.isOpen ||
discuss.thread !== thread ||
!discuss.isReplyingToMessage
) {
return;
if (this.discussAsReplying) {
this.discussAsReplying.clearReplyingToMessage();
}
discuss.clearReplyingToMessage();
}
focus() {
@@ -509,6 +506,14 @@ function factory(dependencies) {
],
default: false,
}),
/**
* Instance of discuss if this composer is used as the reply composer
* from Inbox. This field is computed from the inverse relation and
* should be considered read-only.
*/
discussAsReplying: one2one('mail.discuss', {
inverse: 'replyingToMessageOriginThreadComposer',
}),
extraSuggestedPartners: many2many('mail.partner', {
compute: '_computeExtraSuggestedPartners',
dependencies: [
@@ -176,11 +176,22 @@ function factory(dependencies) {
this.update({ renamingThreads: [['unlink', thread]] });
}
/**
* Action to initiate reply to given message in Inbox. Assumes that
* Discuss and Inbox are already opened.
*
* @param {mail.message} message
*/
replyToMessage(message) {
this.update({ replyingToMessage: [['link', message]] });
this.replyingToMessageOriginThreadComposer.focus();
}
/**
* @param {mail.thread} thread
*/
setThreadRenaming(thread) {
this.update({ renamingThreads: [['link', thread ]] });
this.update({ renamingThreads: [['link', thread]] });
}
/**
@@ -262,6 +273,8 @@ function factory(dependencies) {
}
/**
* Ensures the reply feature is disabled if discuss is not open.
*
* @private
* @returns {mail.message|undefined}
*/
@@ -400,9 +413,32 @@ function factory(dependencies) {
],
}),
renamingThreads: one2many('mail.thread'),
replyingToMessage: one2one('mail.message', {
/**
* The message that is currently selected as being replied to in Inbox.
* There is only one reply composer shown at a time, which depends on
* this selected message.
*/
replyingToMessage: many2one('mail.message', {
compute: '_computeReplyingToMessage',
dependencies: ['isOpen'],
dependencies: [
'isOpen',
'replyingToMessage',
],
}),
/**
* The thread concerned by the reply feature in Inbox. It depends on the
* message set to be replied, and should be considered read-only.
*/
replyingToMessageOriginThread: many2one('mail.thread', {
related: 'replyingToMessage.originThread',
}),
/**
* The composer to display for the reply feature in Inbox. It depends
* on the message set to be replied, and should be considered read-only.
*/
replyingToMessageOriginThreadComposer: one2one('mail.composer', {
inverse: 'discussAsReplying',
related: 'replyingToMessageOriginThread.composer',
}),
/**
* Quick search input value in the discuss sidebar (desktop). Useful
@@ -277,18 +277,11 @@ function factory(dependencies) {
}
/**
* Action to initiate reply to given message.
* Action to initiate reply to current message in Discuss Inbox. Assumes
* that Discuss and Inbox are already opened.
*/
replyTo() {
const discuss = this.env.messaging.discuss;
if (!discuss.isOpen) {
return;
}
if (discuss.replyingToMessage === this) {
discuss.clearReplyingToMessage();
} else {
discuss.update({ replyingToMessage: [['link', this]] });
}
this.env.messaging.discuss.replyToMessage(this);
}
/**
+32
View File
@@ -12,6 +12,36 @@ const {
//------------------------------------------------------------------------------
const classPatchMap = new WeakMap();
const eventHandledWeakMap = new WeakMap();
/**
* Returns whether the given event has been handled with the given markName.
*
* @param {Event} ev
* @param {string} markName
* @returns {boolean}
*/
function isEventHandled(ev, markName) {
if (!eventHandledWeakMap.get(ev)) {
return false;
}
return eventHandledWeakMap.get(ev).includes(markName);
}
/**
* Marks the given event as handled by the given markName. Useful to allow
* handlers in the propagation chain to make a decision based on what has
* already been done.
*
* @param {Event} ev
* @param {string} markName
*/
function markEventHandled(ev, markName) {
if (!eventHandledWeakMap.get(ev)) {
eventHandledWeakMap.set(ev, []);
}
eventHandledWeakMap.get(ev).push(markName);
}
/**
* Wait a task tick, so that anything in micro-task queue that can be processed
@@ -117,6 +147,8 @@ function unpatchInstanceMethods(Class, patchName) {
//------------------------------------------------------------------------------
return {
isEventHandled,
markEventHandled,
nextTick,
patchClassMethods,
patchInstanceMethods,
+1
View File
@@ -244,6 +244,7 @@
<script type="text/javascript" src="/mail/static/src/components/chatter/chatter_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/composer/composer_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_inbox_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_moderation_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_pinned_tests.js"></script>
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_tests.js"></script>