[FIX] mail, *: significantly reduce number of useless renders

* = hr_holidays, im_livechat, website_livechat

A huge amount of components where rendered for no reason, notably when typing a
new message (at every key press), which significantly slowed down the interface.

There were 2 causes to this number of renders:
- OWL rendering all children automatically, even if their props didn't change,
  unless `shouldUpdate` is overridden, which is now done.
- `useStore` selectors being way too generous in what they observed

task-2399731

closes odoo/odoo#64958

X-original-commit: a0472425251d429a0828eff1675c84b3f24527d8
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2021-01-22 17:53:15 +00:00
parent ea4e594b28
commit 9308ba3dce
73 changed files with 649 additions and 275 deletions
@@ -17,11 +17,10 @@ patch(components.ThreadView, 'hr_holidays/static/src/components/thread_view/thre
*/
_useStoreSelector(props) {
const res = this._super(...arguments);
const threadView = this.env.models['mail.thread_view'].get(props.threadViewLocalId);
const thread = threadView ? threadView.thread : undefined;
const correspondent = thread ? thread.correspondent : undefined;
const thread = res.thread;
const correspondent = thread && thread.correspondent;
return Object.assign({}, res, {
correspondent: correspondent ? correspondent.__state : undefined,
correspondentOutOfOfficeText: correspondent && correspondent.outOfOfficeText,
});
},
});
@@ -69,15 +69,8 @@ patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_side
*/
_useStoreSelector(props) {
return Object.assign(this._super(...arguments), {
allOrderedAndPinnedLivechats: this.env.models['mail.thread']
.all(thread =>
thread.channel_type === 'livechat' &&
thread.isPinned &&
thread.model === 'mail.channel'
)
.map(livechat => livechat.__state),
}
);
allOrderedAndPinnedLivechats: this.quickSearchOrderedAndPinnedLivechatList(),
});
},
});
-4
View File
@@ -4,7 +4,3 @@
* versions of Odoo. Any module that is defined in this file should be isolated
* in its own file in master.
*/
odoo.define('mail/static/src/bugfix/bugfix.js', function (require) {
'use strict';
});
@@ -0,0 +1,72 @@
odoo.define('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js', function (require) {
'use strict';
const { Component } = owl;
/**
* Compares `a` and `b` up to the given `compareDepth`.
*
* @param {any} a
* @param {any} b
* @param {Object|integer} compareDepth
* @returns {boolean}
*/
function isEqual(a, b, compareDepth) {
const keys = Object.keys(a);
if (Object.keys(b).length !== keys.length) {
return false;
}
for (const key of keys) {
// the depth can be given either as a number (for all keys) or as
// an object (for each key)
let depth;
if (typeof compareDepth === 'number') {
depth = compareDepth;
} else {
depth = compareDepth[key] || 0;
}
if (depth === 0 && a[key] !== b[key]) {
return false;
}
if (depth !== 0) {
let nextDepth;
if (typeof depth === 'number') {
nextDepth = depth - 1;
} else {
nextDepth = depth;
}
if (!isEqual(a[key], b[key], nextDepth)) {
return false;
}
}
}
return true;
}
/**
* This hook overrides the `shouldUpdate` method to ensure the component is only
* updated if its props actually changed. This is especially useful to use on
* components for which an extra render costs proportionally a lot more than
* comparing props.
*
* @param {Object} [param0={}]
* @param {Object} [param0.compareDepth={}] allows to specify the comparison
* depth to use for each prop. Default is shallow compare (depth = 0).
*/
function useShouldUpdateBasedOnProps({ compareDepth = {} } = {}) {
const component = Component.current;
component.shouldUpdate = nextProps => {
const allNewProps = Object.assign({}, nextProps);
const defaultProps = component.constructor.defaultProps;
for (const key in defaultProps) {
if (allNewProps[key] === undefined) {
allNewProps[key] = defaultProps[key];
}
}
return !isEqual(component.props, allNewProps, compareDepth);
};
}
return useShouldUpdateBasedOnProps;
});
@@ -6,6 +6,7 @@ const components = {
FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'),
MailTemplate: require('mail/static/src/components/mail_template/mail_template.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const {
@@ -24,6 +25,7 @@ class Activity extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.state = useState({
areDetailsVisible: false,
});
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/activity_box/activity_box.js', function
const components = {
Activity: require('mail/static/src/components/activity/activity.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class ActivityBox extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatter = this.env.models['mail.chatter'].get(props.chatterLocalId);
const thread = chatter && chatter.thread;
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -13,6 +14,7 @@ class ActivityMarkDonePopover extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const activity = this.env.models['mail.activity'].get(props.activityLocalId);
return {
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/attachment/attachment.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
@@ -16,6 +17,11 @@ class Attachment extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
attachmentLocalIds: 1,
},
});
useStore(props => {
const attachment = this.env.models['mail.attachment'].get(props.attachmentLocalId);
return {
@@ -7,6 +7,7 @@ const components = {
FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'),
};
const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -20,14 +21,19 @@ class AttachmentBox extends Component {
constructor(...args) {
super(...args);
this.isDropZoneVisible = useDragVisibleDropZone();
useShouldUpdateBasedOnProps();
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
return {
attachments: thread
? thread.allAttachments.map(attachment => attachment.__state)
: [],
thread: thread ? thread.__state : undefined,
thread,
threadAllAttachments: thread ? thread.allAttachments : [],
threadId: thread && thread.id,
threadModel: thread && thread.model,
};
}, {
compareDepth: {
threadAllAttachments: 1,
},
});
/**
* Reference of the file uploader.
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
@@ -17,6 +18,7 @@ class AttachmentDeleteConfirmDialog extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const attachment = this.env.models['mail.attachment'].get(props.attachmentLocalId);
return {
@@ -4,16 +4,24 @@ odoo.define('mail/static/src/components/attachment_list/attachment_list.js', fun
const components = {
Attachment: require('mail/static/src/components/attachment/attachment.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
class AttachmentList extends Component {
/**
* @override
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
attachmentLocalIds: 1,
},
});
useStore(props => {
const attachments = this.env.models['mail.attachment'].all().filter(attachment =>
props.attachmentLocalIds.includes(attachment.localId)
@@ -23,6 +31,10 @@ class AttachmentList extends Component {
? attachments.map(attachment => attachment.__state)
: undefined,
};
}, {
compareDepth: {
attachments: 1,
},
});
}
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/attachment_viewer/attachment_viewer.js',
'use strict';
const useRefs = require('mail/static/src/component_hooks/use_refs/use_refs.js');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component, QWeb } = owl;
@@ -19,6 +20,7 @@ class AttachmentViewer extends Component {
constructor(...args) {
super(...args);
this.MIN_SCALE = MIN_SCALE;
useShouldUpdateBasedOnProps();
useStore(props => {
const attachmentViewer = this.env.models['mail.attachment_viewer'].get(props.localId);
return {
@@ -1,10 +1,17 @@
odoo.define('mail/static/src/components/autocomplete_input/autocomplete_input.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const { Component } = owl;
class AutocompleteInput extends Component {
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
}
mounted() {
if (this.props.isFocusOnMount) {
this.el.focus();
@@ -6,6 +6,7 @@ const components = {
ChatWindowHeader: require('mail/static/src/components/chat_window_header/chat_window_header.js'),
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { isEventHandled } = require('mail/static/src/utils/utils.js');
@@ -22,14 +23,24 @@ class ChatWindow extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatWindow = this.env.models['mail.chat_window'].get(props.chatWindowLocalId);
const thread = chatWindow ? chatWindow.thread : undefined;
return {
chatWindow: chatWindow ? chatWindow.__state : undefined,
chatWindow,
chatWindowHasNewMessageForm: chatWindow && chatWindow.hasNewMessageForm,
chatWindowIsDoFocus: chatWindow && chatWindow.isDoFocus,
chatWindowIsFocused: chatWindow && chatWindow.isFocused,
chatWindowIsFolded: chatWindow && chatWindow.isFolded,
chatWindowThreadView: chatWindow && chatWindow.threadView,
chatWindowVisibleIndex: chatWindow && chatWindow.visibleIndex,
chatWindowVisibleOffset: chatWindow && chatWindow.visibleOffset,
isDeviceMobile: this.env.messaging.device.isMobile,
localeTextDirection: this.env.messaging.locale.textDirection,
thread: thread ? thread.__state : undefined,
thread,
threadMassMailing: thread && thread.mass_mailing,
threadModel: thread && thread.model,
};
});
useUpdate({ func: () => this._update() });
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/chat_window_header/chat_window_header.js
const components = {
ThreadIcon: require('mail/static/src/components/thread_icon/thread_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const {
isEventHandled,
@@ -19,13 +20,21 @@ class ChatWindowHeader extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatWindow = this.env.models['mail.chat_window'].get(props.chatWindowLocalId);
const thread = chatWindow && chatWindow.thread;
const correspondent = thread && thread.correspondent;
return {
chatWindow: chatWindow ? chatWindow.__state : undefined,
chatWindow,
chatWindowHasShiftNext: chatWindow && chatWindow.hasShiftNext,
chatWindowHasShiftPrev: chatWindow && chatWindow.hasShiftPrev,
chatWindowName: chatWindow && chatWindow.name,
correspondent,
isDeviceMobile: this.env.messaging.device.isMobile,
thread: thread ? thread.__state : undefined,
thread,
threadLocalMessageUnreadCounter: thread && thread.localMessageUnreadCounter,
threadMassMailing: thread && thread.mass_mailing,
};
});
}
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/chat_window_hidden_menu/chat_window_hidd
const components = {
ChatWindowHeader: require('mail/static/src/components/chat_window_header/chat_window_header.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -5,6 +5,7 @@ const components = {
ChatWindow: require('mail/static/src/components/chat_window/chat_window.js'),
ChatWindowHiddenMenu: require('mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -16,19 +17,23 @@ class ChatWindowManager extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatWindowManager = this.env.messaging && this.env.messaging.chatWindowManager;
const allOrderedVisible = chatWindowManager
? chatWindowManager.allOrderedVisible
: [];
return {
allOrderedVisible: allOrderedVisible.map(chatWindow => chatWindow ? chatWindow.__state : undefined),
chatWindowManager: chatWindowManager ? chatWindowManager.__state : undefined,
allOrderedVisible,
allOrderedVisibleThread: allOrderedVisible.map(chatWindow => chatWindow.thread),
chatWindowManager,
chatWindowManagerHasHiddenChatWindows: chatWindowManager && chatWindowManager.hasHiddenChatWindows,
isMessagingInitialized: this.env.isMessagingInitialized(),
};
}, {
compareDepth: {
allOrderedVisible: 1,
allOrderedVisibleThread: 1,
},
});
}
@@ -855,11 +855,12 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_NotificationList_preview').click(),
message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -871,10 +872,11 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142;
},
message: "should wait until channel 20 scrolled to 142 after setting this value manually",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -890,10 +892,11 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a
eventName: 'o-component-message-list-scrolled',
func: () => this.showHomeMenu(),
message: "should wait until channel 20 restored its scroll to 142 after showing the home menu",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -1623,11 +1626,12 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_NotificationList_preview').click(),
message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1639,10 +1643,11 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f
document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142;
},
message: "should wait until channel 20 scrolled to 142 after setting this value manually",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -1666,10 +1671,11 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_ChatWindow_header').click(),
message: "should wait until channel 20 restored its scroll position to 142",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
@@ -1823,11 +1829,12 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_NotificationList_preview').click(),
message: "should wait until channel 20 scrolled to its last message after opening it from the messaging menu",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1837,10 +1844,11 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142,
message: "should wait until channel 20 scrolled to 142 after setting this value manually",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -1853,10 +1861,11 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_ChatWindow_header').click(),
message: "should wait until channel 20 restored its scroll to 142 after unfolding it",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -1876,10 +1885,11 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector('.o_ChatWindow_header').click(),
message: "should wait until channel 20 restored its scroll position to the last saved value (142)",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 142
);
},
@@ -8,6 +8,7 @@ const components = {
Composer: require('mail/static/src/components/composer/composer.js'),
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -21,6 +22,7 @@ class Chatter extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatter = this.env.models['mail.chatter'].get(props.chatterLocalId);
const thread = chatter ? chatter.thread : undefined;
@@ -31,7 +33,9 @@ class Chatter extends Component {
return {
attachments: attachments.map(attachment => attachment.__state),
chatter: chatter ? chatter.__state : undefined,
thread: thread ? thread.__state : undefined,
composer: thread && thread.composer,
thread,
threadActivitiesLength: thread && thread.activities.length,
};
}, {
compareDepth: {
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/chatter_container/chatter_container.js',
const components = {
Chatter: require('mail/static/src/components/chatter/chatter.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { clear } = require('mail/static/src/model/model_field_command.js');
@@ -27,6 +28,7 @@ class ChatterContainer extends Component {
super(...args);
this.chatter = undefined;
this._wasMessagingInitialized = false;
useShouldUpdateBasedOnProps();
useStore(props => {
const isMessagingInitialized = this.env.isMessagingInitialized();
// Delay creation of chatter record until messaging is initialized.
@@ -5,6 +5,7 @@ const components = {
FollowButton: require('mail/static/src/components/follow_button/follow_button.js'),
FollowerListMenu: require('mail/static/src/components/follower_list_menu/follower_list_menu.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -16,6 +17,7 @@ class ChatterTopbar extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const chatter = this.env.models['mail.chatter'].get(props.chatterLocalId);
const thread = chatter ? chatter.thread : undefined;
@@ -23,7 +25,7 @@ class ChatterTopbar extends Component {
return {
areThreadAttachmentsLoaded: thread && thread.areAttachmentsLoaded,
chatter: chatter ? chatter.__state : undefined,
composer: chatter && chatter.composer ? chatter.composer.__state : undefined,
composerIsLog: chatter && chatter.composer && chatter.composer.isLog,
threadAttachmentsAmount: threadAttachments.length,
};
});
@@ -11,8 +11,9 @@ const components = {
ThreadTextualTypingStatus: require('mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js'),
};
const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const {
isEventHandled,
markEventHandled,
@@ -29,15 +30,33 @@ class Composer extends Component {
constructor(...args) {
super(...args);
this.isDropZoneVisible = useDragVisibleDropZone();
useShouldUpdateBasedOnProps({
compareDepth: {
textInputSendShortcuts: 1,
},
});
useStore(props => {
const composer = this.env.models['mail.composer'].get(props.composerLocalId);
const thread = composer && composer.thread;
return {
composer: composer ? composer.__state : undefined,
composer,
composerAttachments: composer ? composer.attachments : [],
composerCanPostMessage: composer && composer.canPostMessage,
composerHasFocus: composer && composer.hasFocus,
composerIsLog: composer && composer.isLog,
composerSubjectContent: composer && composer.subjectContent,
isDeviceMobile: this.env.messaging.device.isMobile,
thread: composer && composer.thread
? composer.thread.__state
: undefined,
thread,
threadChannelType: thread && thread.channel_type, // for livechat override
threadDisplayName: thread && thread.displayName,
threadMassMailing: thread && thread.mass_mailing,
threadModel: thread && thread.model,
threadName: thread && thread.name,
};
}, {
compareDepth: {
composerAttachments: 1,
},
});
useUpdate({ func: () => this._update() });
/**
@@ -198,6 +217,9 @@ class Composer extends Component {
* @private
*/
_update() {
if (this.props.isDoFocus) {
this.focus();
}
if (!this.composer) {
return;
}
@@ -364,7 +386,6 @@ class Composer extends Component {
Object.assign(Composer, {
components,
defaultProps: {
attachmentLocalIds: [],
hasCurrentPartnerAvatar: true,
hasDiscardButton: false,
hasFollowers: false,
@@ -372,13 +393,10 @@ Object.assign(Composer, {
hasThreadName: false,
hasThreadTyping: false,
isCompact: true,
isDoFocus: false,
isExpandable: false,
},
props: {
attachmentLocalIds: {
type: Array,
element: String,
},
attachmentsDetailsMode: {
type: String,
optional: true,
@@ -394,6 +412,10 @@ Object.assign(Composer, {
hasSendButton: Boolean,
hasThreadName: Boolean,
hasThreadTyping: Boolean,
/**
* Determines whether this should become focused.
*/
isDoFocus: Boolean,
showAttachmentsExtensions: {
type: Boolean,
optional: true,
@@ -402,15 +424,6 @@ Object.assign(Composer, {
type: Boolean,
optional: true,
},
initialAttachmentLocalIds: {
type: Array,
element: String,
optional: true,
},
initialTextInputHtmlContent: {
type: String,
optional: true,
},
isCompact: Boolean,
isExpandable: Boolean,
/**
@@ -820,8 +820,6 @@ QUnit.test("channel with no commands should not prompt any command suggestions o
await afterNextRender(() => {
document.querySelector('.o_ComposerTextInput_textarea').focus();
document.execCommand('insertText', false, "/");
});
await afterNextRender(() => {
const composer_text_input = document.querySelector('.o_ComposerTextInput_textarea');
composer_text_input.dispatchEvent(new window.KeyboardEvent('keydown'));
composer_text_input.dispatchEvent(new window.KeyboardEvent('keyup'));
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -30,7 +31,7 @@ class ComposerSuggestedRecipient extends Component {
constructor(...args) {
super(...args);
this.id = _.uniqueId('o_ComposerSuggestedRecipient_');
useShouldUpdateBasedOnProps();
useStore(props => {
const suggestedRecipientInfo = this.env.models['mail.suggested_recipient_info'].get(props.suggestedRecipientLocalId);
const partner = suggestedRecipientInfo && suggestedRecipientInfo.partner;
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -17,15 +18,19 @@ class ComposerSuggestedRecipientList extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.state = useState({
hasShowMoreButton: false,
});
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
return {
thread: thread ? thread.__state : undefined,
threadSuggestedRecipientInfoList: thread ? thread.suggestedRecipientInfoList : [],
};
}, {
compareDepth: {
threadSuggestedRecipientInfoList: 1,
},
});
}
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -17,11 +18,12 @@ class ComposerSuggestion extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const composer = this.env.models['mail.composer'].get(this.props.composerLocalId);
const record = this.env.models[props.modelName].get(props.recordLocalId);
return {
composer: composer && composer.__state,
composerHasToScrollToActiveSuggestion: composer && composer.hasToScrollToActiveSuggestion,
record: record ? record.__state : undefined,
};
});
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/composer_suggestion_list/composer_sugges
const components = {
ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class ComposerSuggestionList extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const composer = this.env.models['mail.composer'].get(props.composerLocalId);
const activeSuggestedRecord = composer
@@ -27,14 +29,11 @@ class ComposerSuggestionList extends Component {
? composer.mainSuggestedRecordsList
: [];
return {
activeSuggestedRecord: activeSuggestedRecord ? activeSuggestedRecord.__state : undefined,
composer: composer ? composer.__state : undefined,
extraSuggestedRecordsList: extraSuggestedRecordsList
? extraSuggestedRecordsList.map(record => record.__state)
: [],
mainSuggestedRecordsList: mainSuggestedRecordsList
? mainSuggestedRecordsList.map(record => record.__state)
: [],
activeSuggestedRecord,
composer,
composerSuggestionModelName: composer && composer.suggestionModelName,
extraSuggestedRecordsList: extraSuggestedRecordsList,
mainSuggestedRecordsList: mainSuggestedRecordsList,
};
}, {
compareDepth: {
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/composer_text_input/composer_text_input.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -12,9 +13,6 @@ const { markEventHandled } = require('mail/static/src/utils/utils.js');
const { Component } = owl;
const { useRef } = owl.hooks;
/**
* ComposerInput relies on a minimal HTML editor in order to support mentions.
*/
class ComposerTextInput extends Component {
/**
@@ -22,15 +20,29 @@ class ComposerTextInput extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
sendShortcuts: 1,
},
});
useStore(props => {
const composer = this.env.models['mail.composer'].get(props.composerLocalId);
const thread = composer && composer.thread;
const correspondent = thread ? thread.correspondent : undefined;
return {
composer: composer ? composer.__state : undefined,
correspondent: correspondent ? correspondent.__state : undefined,
composerHasFocus: composer && composer.hasFocus,
composerHasSuggestions: composer && composer.hasSuggestions,
composerIsLastStateChangeProgrammatic: composer && composer.isLastStateChangeProgrammatic,
composerIsLog: composer && composer.isLog,
composerTextInputContent: composer && composer.textInputContent,
composerTextInputCursorEnd: composer && composer.textInputCursorEnd,
composerTextInputCursorStart: composer && composer.textInputCursorStart,
composerTextInputSelectionDirection: composer && composer.textInputSelectionDirection,
correspondent,
correspondentNameOrDisplayName: correspondent && correspondent.nameOrDisplayName,
isDeviceMobile: this.env.messaging.device.isMobile,
thread: thread ? thread.__state : undefined,
threadDisplayName: thread && thread.displayName,
threadModel: thread && thread.model,
};
});
/**
@@ -78,7 +90,7 @@ class ComposerTextInput extends Component {
}
if (this.composer.thread.model === 'mail.channel') {
if (this.composer.thread.correspondent) {
return _.str.sprintf("Message %s...", this.composer.thread.correspondent.nameOrDisplayName)
return _.str.sprintf("Message %s...", this.composer.thread.correspondent.nameOrDisplayName);
}
return _.str.sprintf("Message #%s...", this.composer.thread.displayName);
}
@@ -196,6 +208,7 @@ class ComposerTextInput extends Component {
*/
_onFocusinTextarea() {
this.composer.focus();
this.trigger('o-focusin-composer');
}
/**
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/dialog/dialog.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const patchMixin = require('web.patchMixin');
@@ -21,6 +22,7 @@ class Dialog extends Component {
this._componentRef = useRef('component');
this._onClickGlobal = this._onClickGlobal.bind(this);
this._onKeydownDocument = this._onKeydownDocument.bind(this);
useShouldUpdateBasedOnProps();
useStore(props => {
const dialog = this.env.models['mail.dialog'].get(props.dialogLocalId);
return {
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/dialog_manager/dialog_manager.js', funct
const components = {
Dialog: require('mail/static/src/components/dialog/dialog.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class DialogManager extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const dialogManager = this.env.messaging && this.env.messaging.dialogManager;
return {
@@ -12,8 +12,10 @@ const components = {
NotificationList: require('mail/static/src/components/notification_list/notification_list.js'),
ThreadView: require('mail/static/src/components/thread_view/thread_view.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const patchMixin = require('web.patchMixin');
const { Component } = owl;
const { useRef } = owl.hooks;
@@ -25,27 +27,13 @@ class Discuss extends Component {
*/
constructor(...args) {
super(...args);
useStore(props => {
const discuss = this.env.messaging && this.env.messaging.discuss;
const threadView = discuss && discuss.threadView;
return {
checkedMessages: threadView ? threadView.checkedMessages.map(message => message.__state) : [],
discuss: discuss ? discuss.__state : undefined,
isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
isMessagingInitialized: this.env.isMessagingInitialized(),
thread: discuss && discuss.thread ? discuss.thread.__state : undefined,
threadCache: (threadView && threadView.threadCache)
? threadView.threadCache.__state
: undefined,
uncheckedMessages: threadView ? threadView.uncheckedMessages.map(message => message.__state) : [],
};
}, {
useShouldUpdateBasedOnProps();
useStore((...args) => this._useStoreSelector(...args), {
compareDepth: {
checkedMessages: 1,
uncheckedMessages: 1,
},
});
useUpdate({ func: () => this._update() });
this._updateLocalStoreProps();
/**
* Reference of the composer. Useful to focus it.
@@ -64,7 +52,7 @@ class Discuss extends Component {
this.discuss.update({ isOpen: true });
if (this.discuss.thread) {
this.trigger('o-push-state-action-manager');
} else if (this.env.messaging.isInitialized) {
} else if (this.env.isMessagingInitialized()) {
this.discuss.openInitThread();
}
this._updateLocalStoreProps();
@@ -142,27 +130,6 @@ class Discuss extends Component {
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_update() {
if (!this.discuss) {
return;
}
if (this.discuss.isDoFocus) {
this.discuss.update({ isDoFocus: false });
const composer = this._composerRef.comp;
if (composer) {
composer.focus();
} else {
const threadView = this._threadViewRef.comp;
if (threadView) {
threadView.focus();
}
}
}
}
/**
* @private
*/
@@ -187,6 +154,50 @@ class Discuss extends Component {
);
}
/**
* Returns data selected from the store.
*
* @private
* @param {Object} props
* @returns {Object}
*/
_useStoreSelector(props) {
const discuss = this.env.messaging && this.env.messaging.discuss;
const thread = discuss && discuss.thread;
const threadView = discuss && discuss.threadView;
const replyingToMessage = discuss && discuss.replyingToMessage;
const replyingToMessageOriginThread = replyingToMessage && replyingToMessage.originThread;
const checkedMessages = threadView ? threadView.checkedMessages : [];
return {
checkedMessages,
checkedMessagesIsModeratedByCurrentPartner: checkedMessages && checkedMessages.some(message => message.isModeratedByCurrentPartner), // for widget
discuss,
discussActiveId: discuss && discuss.activeId, // for widget
discussActiveMobileNavbarTabId: discuss && discuss.activeMobileNavbarTabId,
discussHasModerationDiscardDialog: discuss && discuss.hasModerationDiscardDialog,
discussHasModerationRejectDialog: discuss && discuss.hasModerationRejectDialog,
discussIsAddingChannel: discuss && discuss.isAddingChannel,
discussIsAddingChat: discuss && discuss.isAddingChat,
discussIsDoFocus: discuss && discuss.isDoFocus,
discussReplyingToMessageOriginThreadComposer: replyingToMessageOriginThread && replyingToMessageOriginThread.composer,
inbox: this.env.messaging.inbox,
isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
isMessagingInitialized: this.env.isMessagingInitialized(),
replyingToMessage,
starred: this.env.messaging.starred, // for widget
thread,
threadCache: threadView && threadView.threadCache,
threadChannelType: thread && thread.channel_type, // for widget
threadDisplayName: thread && thread.displayName, // for widget
threadCounter: thread && thread.counter,
threadModel: thread && thread.model,
threadPublic: thread && thread.public, // for widget
threadView,
threadViewMessagesLength: threadView && threadView.messages.length, // for widget
uncheckedMessages: threadView ? threadView.uncheckedMessages : [], // for widget
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -205,6 +216,14 @@ class Discuss extends Component {
this.discuss.update({ hasModerationRejectDialog: false });
}
/**
* @private
* @param {CustomEvent} ev
*/
_onFocusinComposer(ev) {
this.discuss.update({ isDoFocus: false });
}
/**
* @private
* @param {CustomEvent} ev
@@ -289,6 +308,6 @@ Object.assign(Discuss, {
template: 'mail.Discuss',
});
return Discuss;
return patchMixin(Discuss);
});
@@ -60,7 +60,9 @@
hasSquashCloseMessages="discuss.thread.model !== 'mail.box'"
haveMessagesMarkAsReadIcon="discuss.thread === env.messaging.inbox"
haveMessagesReplyIcon="discuss.thread === env.messaging.inbox"
isDoFocus="discuss.isDoFocus"
threadViewLocalId="discuss.threadView.localId"
t-on-o-focusin-composer="_onFocusinComposer"
t-on-o-rendered="_onThreadRendered"
t-ref="threadView"
/>
@@ -91,7 +93,9 @@
hasCurrentPartnerAvatar="!env.messaging.device.isMobile"
hasDiscardButton="true"
hasThreadName="true"
isDoFocus="discuss.isDoFocus"
textInputSendShortcuts="['ctrl-enter', 'meta-enter']"
t-on-o-focusin-composer="_onFocusinComposer"
t-on-o-message-posted="_onReplyingToMessageMessagePosted"
t-ref="composer"
/>
@@ -1709,11 +1709,12 @@ QUnit.test('auto-scroll to bottom of thread', async function (assert) {
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until channel 20 scrolled to its last message initially",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1756,11 +1757,12 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until channel 20 scrolled to its last message initially",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1844,11 +1846,12 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until channel 20 scrolled to its last message initially",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1872,10 +1875,11 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0;
},
message: "should wait until channel scrolled to top",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === 0
);
},
@@ -1912,11 +1916,12 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) {
messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight;
},
message: "should wait until channel scrolled to bottom",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -1973,8 +1978,8 @@ QUnit.test('restore thread scroll position', async function (assert) {
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until channel 11 scrolled to its last message",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
predicate: ({ thread }) => {
return thread && thread.model === 'mail.channel' && thread.id === 11;
},
},
});
@@ -1999,8 +2004,8 @@ QUnit.test('restore thread scroll position', async function (assert) {
eventName: 'o-component-message-list-scrolled',
func: () => document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`).scrollTop = 0,
message: "should wait until channel 11 changed its scroll position to top",
predicate: ({ threadViewer }) => {
return threadViewer.thread.model === 'mail.channel' && threadViewer.thread.id === 11;
predicate: ({ thread }) => {
return thread && thread.model === 'mail.channel' && thread.id === 11;
},
});
assert.strictEqual(
@@ -2028,11 +2033,12 @@ QUnit.test('restore thread scroll position', async function (assert) {
`).click();
},
message: "should wait until channel 12 scrolled to its last message",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 12 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 12 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -2060,10 +2066,11 @@ QUnit.test('restore thread scroll position', async function (assert) {
`).click();
},
message: "should wait until channel 11 restored its scroll position",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 11 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 11 &&
scrollTop === 0
);
},
@@ -2089,11 +2096,12 @@ QUnit.test('restore thread scroll position', async function (assert) {
`).click();
},
message: "should wait until channel 12 recovered its scroll position (to bottom)",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 12 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 12 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -3669,11 +3677,12 @@ QUnit.test('load recent messages from thread (already loaded some old messages)'
`).click();
},
message: "should wait until channel scrolled to bottom after opening it from the discuss sidebar",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ scrollTop, thread }) => {
const messageList = document.querySelector('.o_ThreadView_messageList');
return (
threadViewer.thread.model === 'mail.channel' &&
threadViewer.thread.id === 20 &&
thread &&
thread.model === 'mail.channel' &&
thread.id === 20 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
@@ -3975,7 +3984,7 @@ QUnit.test('mark a single message as read should only move this message to "Hist
});
QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) {
assert.expect(4);
assert.expect(2);
const messageOffset = 200;
for (let id = messageOffset; id < messageOffset + 40; id++) {
@@ -3998,22 +4007,18 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
waitUntilEvent: {
eventName: 'o-component-message-list-scrolled',
message: "should wait until inbox scrolled to its last message initially",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ orderedMessages, scrollTop, thread }) => {
const messageList = document.querySelector(`.o_Discuss_thread .o_ThreadView_messageList`);
return (
threadViewer.thread.model === 'mail.box' &&
threadViewer.thread.id === 'inbox' &&
thread &&
thread.model === 'mail.box' &&
thread.id === 'inbox' &&
orderedMessages.length === 30 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
},
});
assert.containsN(
document.body,
'.o_Message',
30,
"there should be 30 messages that are loaded in Inbox"
);
await afterNextRender(async () => {
const markAllReadButton = document.querySelector('.o_widget_Discuss_controlPanelButtonMarkAllRead');
@@ -4035,21 +4040,17 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn
`).click();
},
message: "should wait until history scrolled to its last message after opening it from the discuss sidebar",
predicate: ({ scrollTop, threadViewer }) => {
predicate: ({ orderedMessages, scrollTop, thread }) => {
const messageList = document.querySelector('.o_MessageList');
return (
threadViewer.thread.model === 'mail.box' &&
threadViewer.thread.id === 'history' &&
thread &&
thread.model === 'mail.box' &&
thread.id === 'history' &&
orderedMessages.length === 30 &&
scrollTop === messageList.scrollHeight - messageList.clientHeight
);
},
});
assert.containsN(
document.body,
'.o_Message',
30,
"there should be 30 messages in History"
);
// simulate a scroll to top to load more messages
await this.afterEvent({
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class DiscussMobileMailboxSelection extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
return {
allOrderedAndPinnedMailboxes: this.orderedMailboxes.map(mailbox => mailbox.__state),
@@ -5,6 +5,7 @@ const components = {
AutocompleteInput: require('mail/static/src/components/autocomplete_input/autocomplete_input.js'),
DiscussSidebarItem: require('mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -18,9 +19,10 @@ class DiscussSidebar extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(
(...args) => this._useStoreSelector(...args),
{ compareDepth: () => this._useStoreCompareDepth() }
{ compareDepth: this._useStoreCompareDepth() }
);
useUpdate({ func: () => this._update() });
/**
@@ -150,7 +152,6 @@ class DiscussSidebar extends Component {
allOrderedAndPinnedChats: 1,
allOrderedAndPinnedMailboxes: 1,
allOrderedAndPinnedMultiUserChannels: 1,
allPinnedChannelAmount: 1,
};
}
@@ -160,33 +161,20 @@ class DiscussSidebar extends Component {
* @returns {Object}
*/
_useStoreSelector(props) {
const discuss = this.env.messaging.discuss;
return {
allOrderedAndPinnedChats:
this.env.models['mail.thread']
.all(thread =>
thread.channel_type === 'chat' &&
thread.isPinned &&
thread.model === 'mail.channel'
)
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
.map(chat => chat.__state),
allOrderedAndPinnedMailboxes: this.orderedMailboxes.map(mailbox => mailbox.__state),
allOrderedAndPinnedMultiUserChannels:
this.env.models['mail.thread']
.all(thread =>
thread.channel_type === 'channel' &&
thread.isPinned &&
thread.model === 'mail.channel'
)
.sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1)
.map(channel => channel.__state),
allOrderedAndPinnedChats: this.quickSearchPinnedAndOrderedChats,
allOrderedAndPinnedMailboxes: this.orderedMailboxes,
allOrderedAndPinnedMultiUserChannels: this.quickSearchOrderedAndPinnedMultiUserChannels,
allPinnedChannelAmount:
this.env.models['mail.thread']
.all(thread =>
thread.isPinned &&
thread.model === 'mail.channel'
).length,
discuss: this.env.messaging.discuss.__state,
discussIsAddingChannel: discuss && discuss.isAddingChannel,
discussIsAddingChat: discuss && discuss.isAddingChat,
discussSidebarQuickSearchValue: discuss && discuss.sidebarQuickSearchValue,
};
}
@@ -5,6 +5,7 @@ const components = {
EditableText: require('mail/static/src/components/editable_text/editable_text.js'),
ThreadIcon: require('mail/static/src/components/thread_icon/thread_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { isEventHandled } = require('mail/static/src/utils/utils.js');
@@ -19,13 +20,25 @@ class DiscussSidebarItem extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const discuss = this.env.messaging.discuss;
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
const correspondent = thread ? thread.correspondent : undefined;
return {
correspondent: correspondent ? correspondent.__state : undefined,
discuss: this.env.messaging.discuss.__state,
thread: thread ? thread.__state : undefined,
correspondentName: correspondent && correspondent.name,
discussIsRenamingThread: discuss && discuss.renamingThreads.includes(thread),
isDiscussThread: discuss && discuss.thread === thread,
starred: this.env.messaging.starred,
thread,
threadChannelType: thread && thread.channel_type,
threadCounter: thread && thread.counter,
threadDisplayName: thread && thread.displayName,
threadGroupBasedSubscription: thread && thread.group_based_subscription,
threadLocalMessageUnreadCounter: thread && thread.localMessageUnreadCounter,
threadMassMailing: thread && thread.mass_mailing,
threadMessageNeedactionCounter: thread && thread.message_needaction_counter,
threadModel: thread && thread.model,
};
});
}
@@ -1,6 +1,8 @@
odoo.define('mail/static/src/components/drop_zone/drop_zone.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const { Component, useState } = owl;
class DropZone extends Component {
@@ -10,6 +12,7 @@ class DropZone extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.state = useState({
/**
* Determine whether the user is dragging files over the dropzone.
@@ -2,11 +2,17 @@ odoo.define('mail/static/src/components/editable_text/editable_text.js', functio
'use strict';
const { markEventHandled } = require('mail/static/src/utils/utils.js');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const { Component } = owl;
class EditableText extends Component {
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
}
mounted() {
this.el.focus();
this.el.setSelectionRange(0, (this.el.value && this.el.value.length) || 0);
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/emojis_popover/emojis_popover.js', funct
'use strict';
const emojis = require('mail.emojis');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
const { Component } = owl;
@@ -14,6 +15,7 @@ class EmojisPopover extends Component {
constructor(...args) {
super(...args);
this.emojis = emojis;
useShouldUpdateBasedOnProps();
useUpdate({ func: () => this._update() });
}
@@ -1,6 +1,8 @@
odoo.define('mail/static/src/components/file_uploader/file_uploader.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const core = require('web.core');
const { Component } = owl;
@@ -16,6 +18,12 @@ class FileUploader extends Component {
this._fileInputRef = useRef('fileInput');
this._fileUploadId = _.uniqueId('o_FileUploader_fileupload');
this._onAttachmentUploaded = this._onAttachmentUploaded.bind(this);
useShouldUpdateBasedOnProps({
compareDepth: {
attachmentLocalIds: 1,
newAttachmentExtraData: 3,
},
});
}
mounted() {
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/follow_button/follow_button.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class FollowButton extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.state = useState({
/**
* Determine whether the unfollow button is highlighted or not.
@@ -21,7 +23,7 @@ class FollowButton extends Component {
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
return {
thread: thread ? thread.__state : undefined,
threadIsCurrentPartnerFollowing: thread && thread.isCurrentPartnerFollowing,
};
});
}
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower/follower.js', function (require
const components = {
FollowerSubtypeList: require('mail/static/src/components/follower_subtype_list/follower_subtype_list.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class Follower extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const follower = this.env.models['mail.follower'].get(props.followerLocalId);
return [follower ? follower.__state : undefined];
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower_list_menu/follower_list_menu.js
const components = {
Follower: require('mail/static/src/components/follower/follower.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class FollowerListMenu extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.state = useState({
/**
* Determine whether the dropdown is open or not.
@@ -25,8 +27,8 @@ class FollowerListMenu extends Component {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
const followers = thread ? thread.followers : [];
return {
followers: followers.map(follower => follower.__state),
thread: thread ? thread.__state : undefined,
followers,
threadChannelType: thread && thread.channel_type,
};
}, {
compareDepth: {
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/follower_subtype/follower_subtype.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class FollowerSubtype extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const followerSubtype = this.env.models['mail.follower_subtype'].get(props.followerSubtypeLocalId);
return [followerSubtype ? followerSubtype.__state : undefined];
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/follower_subtype_list/follower_subtype_l
const components = {
FollowerSubtype: require('mail/static/src/components/follower_subtype/follower_subtype.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component, QWeb } = owl;
@@ -15,6 +16,7 @@ class FollowerSubtypeList extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const followerSubtypeList = this.env.models['mail.follower_subtype_list'].get(props.localId);
const follower = followerSubtypeList
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/mail_template/mail_template.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class MailTemplate extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const activity = this.env.models['mail.activity'].get(props.activityLocalId);
const mailTemplate = this.env.models['mail.mail_template'].get(props.mailTemplateLocalId);
@@ -10,6 +10,7 @@ const components = {
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 useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -43,6 +44,7 @@ class Message extends Component {
*/
isClicked: false,
});
useShouldUpdateBasedOnProps();
useStore(props => {
const message = this.env.models['mail.message'].get(props.messageLocalId);
const author = message ? message.author : undefined;
@@ -50,31 +52,37 @@ class Message extends Component {
const originThread = message ? message.originThread : undefined;
const threadView = this.env.models['mail.thread_view'].get(props.threadViewLocalId);
const thread = threadView ? threadView.thread : undefined;
const threadStringifiedDomain = threadView
? threadView.stringifiedDomain
: undefined;
return {
attachments: message
? message.attachments.map(attachment => attachment.__state)
: undefined,
author: author ? author.__state : undefined,
: [],
author,
authorAvatarUrl: author && author.avatarUrl,
authorImStatus: author && author.im_status,
authorNameOrDisplayName: author && author.nameOrDisplayName,
correspondent: thread && thread.correspondent,
hasMessageCheckbox: message ? message.hasCheckbox : false,
isDeviceMobile: this.env.messaging.device.isMobile,
isMessageChecked: message && threadView
? message.isChecked(thread, threadStringifiedDomain)
? message.isChecked(thread, threadView.stringifiedDomain)
: false,
isMessageSelected: message && threadView && threadView.threadViewer
? threadView.threadViewer.selectedMessage === message
: 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,
threadView: threadView ? threadView.__state : undefined,
originThread,
originThreadModel: originThread && originThread.model,
originThreadName: originThread && originThread.name,
originThreadUrl: originThread && originThread.url,
partnerRoot,
thread,
threadHasSeenIndicators: thread && thread.hasSeenIndicators,
threadMassMailing: thread && thread.mass_mailing,
};
}, {
compareDepth: {
attachments: 1,
notifications: 1,
},
});
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/message_author_prefix/message_author_prefix.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class MessageAuthorPrefix extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const message = this.env.models['mail.message'].get(props.messageLocalId);
const author = message ? message.author : undefined;
@@ -6,6 +6,7 @@ const components = {
};
const useRefs = require('mail/static/src/component_hooks/use_refs/use_refs.js');
const useRenderedValues = require('mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js');
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -19,22 +20,30 @@ class MessageList extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const threadView = this.env.models['mail.thread_view'].get(props.threadViewLocalId);
const thread = threadView ? threadView.thread : undefined;
const threadCache = threadView ? threadView.threadCache : undefined;
return {
isDeviceMobile: this.env.messaging.device.isMobile,
messages: threadCache
? threadCache.orderedMessages.map(message => message.__state)
: [],
thread: thread ? thread.__state : undefined,
threadCache: threadCache ? threadCache.__state : undefined,
threadView: threadView ? threadView.__state : undefined,
thread,
threadCache,
threadCacheIsAllHistoryLoaded: threadCache && threadCache.isAllHistoryLoaded,
threadCacheIsLoaded: threadCache && threadCache.isLoaded,
threadCacheIsLoadingMore: threadCache && threadCache.isLoadingMore,
threadCacheLastMessage: threadCache && threadCache.lastMessage,
threadCacheOrderedMessages: threadCache ? threadCache.orderedMessages : [],
threadIsTemporary: thread && thread.isTemporary,
threadMainCache: thread && thread.mainCache,
threadMessageAfterNewMessageSeparator: thread && thread.messageAfterNewMessageSeparator,
threadViewComponentHintList: threadView ? threadView.componentHintList : [],
threadViewNonEmptyMessagesLength: threadView && threadView.nonEmptyMessages.length,
};
}, {
compareDepth: {
messages: 1,
threadCacheOrderedMessages: 1,
threadViewComponentHintList: 1,
},
});
this._getRefs = useRefs();
@@ -63,13 +72,16 @@ class MessageList extends Component {
this._lastRenderedValues = useRenderedValues(() => {
const threadView = this.threadView;
const thread = threadView && threadView.thread;
const threadCache = threadView && threadView.threadCache;
return {
componentHintList: threadView ? [...threadView.componentHintList] : [],
hasAutoScrollOnMessageReceived: threadView && threadView.hasAutoScrollOnMessageReceived,
hasScrollAdjust: this.props.hasScrollAdjust,
mainCache: thread && thread.mainCache,
order: this.props.order,
threadCache: threadView && threadView.threadCache,
orderedMessages: threadCache ? [...threadCache.orderedMessages] : [],
thread,
threadCache,
threadCacheInitialScrollHeight: threadView && threadView.threadCacheInitialScrollHeight,
threadCacheInitialScrollPosition: threadView && threadView.threadCacheInitialScrollPosition,
threadView,
@@ -493,6 +505,8 @@ class MessageList extends Component {
_onScrollThrottled(ev) {
const {
order,
orderedMessages,
thread,
threadCache,
threadView,
threadViewer,
@@ -503,7 +517,9 @@ class MessageList extends Component {
}
const scrollTop = this.el.scrollTop;
this.env.messagingBus.trigger('o-component-message-list-scrolled', {
orderedMessages,
scrollTop,
thread,
threadViewer,
});
if (!this._isLastScrollProgrammatic && threadView && threadView.exists()) {
@@ -37,7 +37,7 @@
</div>
</t>
<!-- LOADING (if order asc)-->
<t t-if="props.order === 'asc' and threadView.messages.length > 0">
<t t-if="props.order === 'asc' and orderedMessages.length > 0">
<t t-call="mail.MessageList.loadMore"/>
</t>
<!-- MESSAGES -->
@@ -75,7 +75,7 @@
</t>
</t>
<!-- LOADING (if order desc)-->
<t t-if="props.order === 'desc' and threadView.messages.length > 0">
<t t-if="props.order === 'desc' and orderedMessages.length > 0">
<t t-call="mail.MessageList.loadMore"/>
</t>
</t>
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/message_seen_indicator/message_seen_indicator.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class MessageSeenIndicator extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const message = this.env.models['mail.message'].get(props.messageLocalId);
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
@@ -6,6 +6,7 @@ const components = {
MobileMessagingNavbar: require('mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js'),
NotificationList: require('mail/static/src/components/notification_list/notification_list.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const patchMixin = require('web.patchMixin');
@@ -25,6 +26,7 @@ class MessagingMenu extends Component {
* item is not considered as a click away from messaging menu in mobile.
*/
this.id = _.uniqueId('o_messagingMenu_');
useShouldUpdateBasedOnProps();
useStore(props => {
return {
isDeviceMobile: this.env.messaging && this.env.messaging.device.isMobile,
@@ -1,10 +1,21 @@
odoo.define('mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const { Component } = owl;
class MobileMessagingNavbar extends Component {
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
tabs: 2,
},
});
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
@@ -17,6 +18,11 @@ class ModerationBanDialog extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
useStore(props => {
const messages = props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
@@ -17,6 +18,11 @@ class ModerationDiscardDialog extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
useStore(props => {
const messages = props.messageLocalIds.map(localId =>
this.env.models['mail.message'].get(localId)
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
@@ -17,6 +18,11 @@ class ModerationRejectDialog extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
messageLocalIds: 1,
},
});
this.state = useState({
title: this.env._t("Message Rejected"),
comment: this.env._t("Your message was rejected by moderator."),
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/notification_alert/notification_alert.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,6 +13,7 @@ class NotificationAlert extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const isMessagingInitialized = this.env.isMessagingInitialized();
return {
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/notification_group/notification_group.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -13,6 +14,7 @@ class NotificationGroup extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const group = this.env.models['mail.notification_group'].get(props.notificationGroupLocalId);
return {
@@ -7,6 +7,7 @@ const components = {
ThreadNeedactionPreview: require('mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js'),
ThreadPreview: require('mail/static/src/components/thread_preview/thread_preview.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -18,6 +19,7 @@ class NotificationList extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
this.storeProps = useStore((...args) => this._useStoreSelector(...args), {
compareDepth: {
// list + notification object created in useStore
@@ -2,6 +2,7 @@ odoo.define('mail/static/src/components/notification_popover/notification_popove
'use strict';
const { Component } = owl;
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
class NotificationPopover extends Component {
@@ -11,6 +12,11 @@ class NotificationPopover extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps({
compareDepth: {
notificationLocalIds: 1,
},
});
useStore(props => {
const notifications = props.notificationLocalIds.map(
notificationLocalId => this.env.models['mail.notification'].get(notificationLocalId)
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/notification_request/notification_reques
const components = {
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,6 +16,7 @@ class NotificationRequest extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
return {
isDeviceMobile: this.env.messaging.device.isMobile,
@@ -1,6 +1,7 @@
odoo.define('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -12,13 +13,13 @@ class PartnerImStatusIcon extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const partner = this.env.models['mail.partner'].get(props.partnerLocalId);
return {
partner: partner ? partner.__state : undefined,
partnerRoot: this.env.messaging.partnerRoot
? this.env.messaging.partnerRoot.__state
: undefined,
partner,
partnerImStatus: partner && partner.im_status,
partnerRoot: this.env.messaging.partnerRoot,
};
});
}
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/thread_icon/thread_icon.js', function (r
const components = {
ThreadTypingIcon: require('mail/static/src/components/thread_typing_icon/thread_typing_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,15 +16,24 @@ class ThreadIcon extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
const correspondent = thread ? thread.correspondent : undefined;
return {
correspondent: correspondent ? correspondent.__state : undefined,
partnerRoot: this.env.messaging.partnerRoot
? this.env.messaging.partnerRoot.__state
: undefined,
thread: thread ? thread.__state : undefined,
correspondent,
correspondentImStatus: correspondent && correspondent.im_status,
history: this.env.messaging.history,
inbox: this.env.messaging.inbox,
moderation: this.env.messaging.moderation,
partnerRoot: this.env.messaging.partnerRoot,
starred: this.env.messaging.starred,
thread,
threadChannelType: thread && thread.channel_type,
threadModel: thread && thread.model,
threadOrderedOtherTypingMembersLength: thread && thread.orderedOtherTypingMembers.length,
threadPublic: thread && thread.public,
threadTypingStatusText: thread && thread.typingStatusText,
};
});
}
@@ -5,6 +5,7 @@ const components = {
MessageAuthorPrefix: require('mail/static/src/components/message_author_prefix/message_author_prefix.js'),
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const mailUtils = require('mail.utils');
@@ -18,6 +19,7 @@ class ThreadNeedactionPreview extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
const mainThreadCache = thread ? thread.mainCache : undefined;
@@ -5,6 +5,7 @@ const components = {
MessageAuthorPrefix: require('mail/static/src/components/message_author_prefix/message_author_prefix.js'),
PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const mailUtils = require('mail.utils');
@@ -18,6 +19,7 @@ class ThreadPreview extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
let lastMessageAuthor;
@@ -4,6 +4,7 @@ odoo.define('mail/static/src/components/thread_textual_typing_status/thread_text
const components = {
ThreadTypingIcon: require('mail/static/src/components/thread_typing_icon/thread_typing_icon.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const { Component } = owl;
@@ -15,10 +16,12 @@ class ThreadTextualTypingStatus extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore(props => {
const thread = this.env.models['mail.thread'].get(props.threadLocalId);
return {
thread: thread ? thread.__state : undefined,
threadOrderedOtherTypingMembersLength: thread && thread.orderedOtherTypingMembersLength,
threadTypingStatusText: thread && thread.typingStatusText,
};
});
}
@@ -1,9 +1,18 @@
odoo.define('mail/static/src/components/thread_typing_icon/thread_typing_icon.js', function (require) {
'use strict';
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const { Component } = owl;
class ThreadTypingIcon extends Component {}
class ThreadTypingIcon extends Component {
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
}
}
Object.assign(ThreadTypingIcon, {
defaultProps: {
@@ -5,6 +5,7 @@ const components = {
Composer: require('mail/static/src/components/composer/composer.js'),
MessageList: require('mail/static/src/components/message_list/message_list.js'),
};
const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js');
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js');
@@ -18,6 +19,7 @@ class ThreadView extends Component {
*/
constructor(...args) {
super(...args);
useShouldUpdateBasedOnProps();
useStore((...args) => this._useStoreSelector(...args));
useUpdate({ func: () => this._update() });
/**
@@ -121,11 +123,18 @@ class ThreadView extends Component {
const threadView = this.env.models['mail.thread_view'].get(props.threadViewLocalId);
const thread = threadView ? threadView.thread : undefined;
const threadCache = threadView ? threadView.threadCache : undefined;
const correspondent = thread && thread.correspondent;
return {
composer: thread && thread.composer,
correspondentId: correspondent && correspondent.id,
isDeviceMobile: this.env.messaging.device.isMobile,
thread: thread ? thread.__state : undefined,
threadCache: threadCache ? threadCache.__state : undefined,
threadView: threadView ? threadView.__state : undefined,
thread,
threadCacheIsLoaded: threadCache && threadCache.isLoaded,
threadIsTemporary: thread && thread.isTemporary,
threadMassMailing: thread && thread.mass_mailing,
threadModel: thread && thread.model,
threadView,
threadViewIsLoading: threadView && threadView.isLoading,
};
}
@@ -140,6 +149,7 @@ Object.assign(ThreadView, {
hasSquashCloseMessages: false,
haveMessagesMarkAsReadIcon: false,
haveMessagesReplyIcon: false,
isDoFocus: false,
order: 'asc',
showComposerAttachmentsExtensions: true,
showComposerAttachmentsFilenames: true,
@@ -175,6 +185,10 @@ Object.assign(ThreadView, {
hasSquashCloseMessages: Boolean,
haveMessagesMarkAsReadIcon: Boolean,
haveMessagesReplyIcon: Boolean,
/**
* Determines whether this should become focused.
*/
isDoFocus: Boolean,
order: {
type: String,
validate: prop => ['asc', 'desc'].includes(prop),
@@ -34,6 +34,7 @@
hasSendButton="props.hasComposerSendButton"
hasThreadTyping="props.hasComposerThreadTyping"
isCompact="(threadView.thread.model === 'mail.channel' and threadView.thread.mass_mailing) ? false : undefined"
isDoFocus="props.isDoFocus"
showAttachmentsExtensions="props.showComposerAttachmentsExtensions"
showAttachmentsFilenames="props.showComposerAttachmentsFilenames"
textInputSendShortcuts="(threadView.thread.model === 'mail.channel' and threadView.thread.mass_mailing) ? ['ctrl-enter', 'meta-enter'] : ['enter']"
@@ -462,24 +462,16 @@ QUnit.test('basic chatter rendering without messages', async function (assert) {
});
QUnit.test('chatter updating', async function (assert) {
assert.expect(3);
assert.expect(1);
this.data['mail.message'].records.push({ body: "not empty", model: 'res.partner', res_id: 12 });
this.data['res.partner'].records.push(
{ display_name: "first partner", id: 11 },
{ display_name: "second partner", id: 12 }
);
const messageFetchChannelDef = makeDeferred();
await this.createView({
data: this.data,
hasView: true,
async mockRPC(route, args) {
const res = await this._super(...arguments);
if (route.includes('message_fetch')) {
messageFetchChannelDef.resolve();
}
return res;
},
// View params
View: FormView,
model: 'res.partner',
@@ -498,27 +490,35 @@ QUnit.test('chatter updating', async function (assert) {
</div>
</form>
`,
waitUntilEvent: {
eventName: 'o-thread-view-hint-processed',
message: "should wait until partner 11 thread loaded messages initially",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread.model === 'res.partner' &&
threadViewer.thread.id === 11
);
},
}
});
assert.containsOnce(
document.body,
'.o_Chatter',
"there should be a chatter"
);
assert.containsNone(
document.body,
'.o_Message',
"there should be no message"
);
await afterNextRender(async () => {
document.querySelector('.o_pager_next').click();
// wait until messages are fetched, ignore other renders that are too early
await messageFetchChannelDef;
await this.afterEvent({
eventName: 'o-thread-view-hint-processed',
func: () => document.querySelector('.o_pager_next').click(),
message: "should wait until partner 12 thread loaded messages after clicking on next",
predicate: ({ hint, threadViewer }) => {
return (
hint.type === 'messages-loaded' &&
threadViewer.thread.model === 'res.partner' &&
threadViewer.thread.id === 12
);
},
});
assert.containsOnce(
document.body,
'.o_Message',
"there should be a message"
"there should be a message in partner 12 thread"
);
});
+1
View File
@@ -59,6 +59,7 @@
<script type="text/javascript" src="/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js"></script>
<script type="text/javascript" src="/mail/static/src/component_hooks/use_refs/use_refs.js"></script>
<script type="text/javascript" src="/mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js"/>
<script type="text/javascript" src="/mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js"/>
<script type="text/javascript" src="/mail/static/src/component_hooks/use_store/use_store.js"></script>
<script type="text/javascript" src="/mail/static/src/component_hooks/use_update/use_update.js"/>
<script type="text/javascript" src="/mail/static/src/components/activity/activity.js"></script>
@@ -6,6 +6,24 @@ const components = {
VisitorBanner: require('website_livechat/static/src/components/visitor_banner/visitor_banner.js'),
};
components.Discuss.patch('website_livechat/static/src/components/discuss/discuss.js', T =>
class extends T {
/**
* @override
*/
_useStoreSelector(props) {
const res = super._useStoreSelector(...arguments);
const thread = res.thread;
const visitor = thread && thread.visitor;
return Object.assign({}, res, {
visitor,
});
}
}
);
Object.assign(components.Discuss.components, {
VisitorBanner: components.VisitorBanner,
});
@@ -14,7 +14,9 @@ class VisitorBanner extends Component {
super(...args);
useStore(props => {
const visitor = this.env.models['website_livechat.visitor'].get(props.visitorLocalId);
const country = visitor && visitor.country;
return {
country: country && country.__state,
visitor: visitor ? visitor.__state : undefined,
};
});