diff --git a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
index 5712639906b..9df37fcb585 100644
--- a/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
+++ b/addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js
@@ -77,8 +77,12 @@ function factory(dependencies) {
}
if (typeof message === 'object') {
switch (message.type) {
+ case 'mail.channel_description_change':
+ return this._handleNotificationChannelDescriptionChanged(message.payload);
case 'mail.channel_joined':
return this._handleNotificationChannelJoined(message.payload);
+ case 'mail.channel_rename':
+ return this._handleNotificationChannelRenamed(message.payload);
}
}
const [, model, id] = channel;
@@ -137,6 +141,20 @@ function factory(dependencies) {
}
}
+ /**
+ * @private
+ * @param {Object} payload
+ * @param {integer} payload.id
+ * @param {String} payload.description
+ */
+ _handleNotificationChannelDescriptionChanged({ id, description }) {
+ const channel = this.messaging.models['mail.thread'].insert({
+ id,
+ model: 'mail.channel',
+ });
+ channel.update({ description });
+ }
+
/**
* @private
* @param {integer} channelId
@@ -194,6 +212,20 @@ function factory(dependencies) {
}
}
+ /**
+ * @private
+ * @param {Object} payload
+ * @param {integer} payload.id
+ * @param {String} payload.name
+ */
+ _handleNotificationChannelRenamed({ id, name }) {
+ const channel = this.messaging.models['mail.thread'].insert({
+ id,
+ model: 'mail.channel',
+ });
+ channel.update({ name });
+ }
+
/**
* @private
* @param {integer} channelId
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index 0de39466e90..6e41e709d07 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -121,6 +121,22 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
+ /**
+ * Changes description of the thread to the given new description.
+ * Only makes sense for channels.
+ *
+ * @param {string} description
+ */
+ async changeDescription(description) {
+ this.update({ description });
+ return this.env.services.rpc({
+ model: 'mail.channel',
+ method: 'channel_change_description',
+ args: [[this.id]],
+ kwargs: { description },
+ });
+ }
+
/**
* @static
* @param {mail.thread} [thread] the concerned thread
@@ -988,14 +1004,15 @@ function factory(dependencies) {
* Renames this thread to the given new name.
* Only makes sense for channels.
*
- * @param {string} newName
+ * @param {string} name
*/
- async rename(newName) {
+ async rename(name) {
+ this.update({ name });
return this.env.services.rpc({
model: 'mail.channel',
method: 'channel_rename',
- args: [this.id],
- kwargs: { name: newName },
+ args: [[this.id]],
+ kwargs: { name },
});
}
@@ -1189,6 +1206,14 @@ function factory(dependencies) {
return ['chat', 'livechat'].includes(this.channel_type);
}
+ /**
+ * @private
+ * @returns {boolean}
+ */
+ _computeIsChannelDescriptionChangeable() {
+ return this.model === 'mail.channel' && this.channel_type === 'channel';
+ }
+
/**
* @private
* @returns {boolean}
@@ -1750,6 +1775,13 @@ function factory(dependencies) {
id: attr({
required: true,
}),
+ /**
+ * Determines whether this description can be changed.
+ * Only makes sense for channels.
+ */
+ isChannelDescriptionChangeable: attr({
+ compute: '_computeIsChannelDescriptionChangeable',
+ }),
/**
* Determines whether this thread can be renamed.
* Only makes sense for channels.
diff --git a/addons/mail/static/src/models/thread_view_topbar/thread_view_topbar.js b/addons/mail/static/src/models/thread_view_topbar/thread_view_topbar.js
index 5abf3525a85..5c31bf6b6a3 100644
--- a/addons/mail/static/src/models/thread_view_topbar/thread_view_topbar.js
+++ b/addons/mail/static/src/models/thread_view_topbar/thread_view_topbar.js
@@ -22,6 +22,11 @@ function factory(dependencies) {
this.onMouseEnterTopBarThreadName = this.onMouseEnterTopBarThreadName.bind(this);
this.onMouseLeaveTopBarThreadName = this.onMouseLeaveTopBarThreadName.bind(this);
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
+ this.onClickTopbarThreadDescription = this.onClickTopbarThreadDescription.bind(this);
+ this.onInputThreadDescriptionInput = this.onInputThreadDescriptionInput.bind(this);
+ this.onKeyDownThreadDescriptionInput = this.onKeyDownThreadDescriptionInput.bind(this);
+ this.onMouseEnterTopBarThreadDescription = this.onMouseEnterTopBarThreadDescription.bind(this);
+ this.onMouseLeaveTopBarThreadDescription = this.onMouseLeaveTopBarThreadDescription.bind(this);
document.addEventListener('click', this._onClickCaptureGlobal, true);
return super._created();
}
@@ -81,6 +86,27 @@ function factory(dependencies) {
});
}
+ /**
+ * Handles click on the "thread description" of this top bar.
+ *
+ * @param {MouseEvent} ev
+ */
+ onClickTopbarThreadDescription(ev) {
+ if (!this.thread || !this.thread.isChannelDescriptionChangeable) {
+ return;
+ }
+ const selection = window.getSelection();
+ this.update({
+ doFocusOnThreadDescriptionInput: true,
+ doSetSelectionDirectionOnThreadDescriptionInput: selection.anchorOffset < selection.focusOffset ? 'forward' : 'backward',
+ doSetSelectionEndOnThreadDescriptionInput: Math.max(selection.focusOffset, selection.anchorOffset),
+ doSetSelectionStartOnThreadDescriptionInput: Math.min(selection.focusOffset, selection.anchorOffset),
+ isEditingThreadDescription: true,
+ isMouseOverThreadDescription: false,
+ pendingThreadDescription: this.thread.description || "",
+ });
+ }
+
/**
* Handles click on the "unstar all" button of Starred box.
*
@@ -98,6 +124,10 @@ function factory(dependencies) {
this.threadNameInputRef.el.focus();
this.update({ doFocusOnThreadNameInput: clear() });
}
+ if (this.doFocusOnThreadDescriptionInput) {
+ this.threadDescriptionInputRef.el.focus();
+ this.update({ doFocusOnThreadDescriptionInput: clear() });
+ }
if (
this.doSetSelectionStartOnThreadNameInput !== undefined &&
this.doSetSelectionEndOnThreadNameInput !== undefined &&
@@ -114,6 +144,22 @@ function factory(dependencies) {
doSetSelectionStartOnThreadNameInput: clear(),
});
}
+ if (
+ this.doSetSelectionStartOnThreadDescriptionInput !== undefined &&
+ this.doSetSelectionEndOnThreadDescriptionInput !== undefined &&
+ this.doSetSelectionDirectionOnThreadDescriptionInput !== undefined
+ ) {
+ this.threadDescriptionInputRef.el.setSelectionRange(
+ this.doSetSelectionStartOnThreadDescriptionInput,
+ this.doSetSelectionEndOnThreadDescriptionInput,
+ this.doSetSelectionDirectionOnThreadDescriptionInput
+ );
+ this.update({
+ doSetSelectionDirectionOnThreadDescriptionInput: clear(),
+ doSetSelectionEndOnThreadDescriptionInput: clear(),
+ doSetSelectionStartOnThreadDescriptionInput: clear(),
+ });
+ }
}
/**
@@ -125,6 +171,15 @@ function factory(dependencies) {
this.update({ pendingThreadName: ev.target.value });
}
+ /**
+ * Handles input on the "thread description" input of this top bar.
+ *
+ * @param {InputEvent} ev
+ */
+ onInputThreadDescriptionInput(ev) {
+ this.update({ pendingThreadDescription: ev.target.value });
+ }
+
/**
* Handles keydown on the "thread name" input of this top bar.
*
@@ -141,6 +196,22 @@ function factory(dependencies) {
}
}
+ /**
+ * Handles keydown on the "thread description" input of this top bar.
+ *
+ * @param {KeyboardEvent} ev
+ */
+ onKeyDownThreadDescriptionInput(ev) {
+ switch (ev.key) {
+ case 'Enter':
+ this._applyThreadChangeDescription();
+ break;
+ case 'Escape':
+ this._discardThreadChangeDescription();
+ break;
+ }
+ }
+
/**
* Handles mouseenter on the "thread name" of this top bar.
*
@@ -153,6 +224,18 @@ function factory(dependencies) {
this.update({ isMouseOverThreadName: true });
}
+ /**
+ * Handles mouseenter on the "thread description" of this top bar.
+ *
+ * @param {MouseEvent} ev
+ */
+ onMouseEnterTopBarThreadDescription(ev) {
+ if (!this.thread || !this.thread.isChannelDescriptionChangeable) {
+ return;
+ }
+ this.update({ isMouseOverThreadDescription: true });
+ }
+
/**
* Handles mouseleave on the "thread name" of this top bar.
*
@@ -162,6 +245,15 @@ function factory(dependencies) {
this.update({ isMouseOverThreadName: false });
}
+ /**
+ * Handles mouseleave on the "thread description" of this top bar.
+ *
+ * @param {MouseEvent} ev
+ */
+ onMouseLeaveTopBarThreadDescription(ev) {
+ this.update({ isMouseOverThreadDescription: false });
+ }
+
//----------------------------------------------------------------------
// Private
//----------------------------------------------------------------------
@@ -183,6 +275,20 @@ function factory(dependencies) {
}
}
+ /**
+ * @private
+ */
+ _applyThreadChangeDescription() {
+ const newDescription = this.pendingThreadDescription || "";
+ this.update({
+ isEditingThreadDescription: false,
+ pendingThreadDescription: clear(),
+ });
+ if (this.thread.channel_type === 'channel' && newDescription !== this.thread.description) {
+ this.thread.changeDescription(newDescription);
+ }
+ }
+
/**
* @private
*/
@@ -193,20 +299,27 @@ function factory(dependencies) {
});
}
+ /**
+ * @private
+ */
+ _discardThreadChangeDescription() {
+ this.update({
+ isEditingThreadDescription: false,
+ pendingThreadDescription: clear(),
+ });
+ }
+
/**
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
- if (!this.threadNameInputRef) {
- return;
- }
- if (this.threadNameInputRef.el && this.threadNameInputRef.el.contains(ev.target)) {
- return;
- }
- if (this.isEditingThreadName) {
+ if (this.isEditingThreadName && this.threadNameInputRef.el && !this.threadNameInputRef.el.contains(ev.target)) {
this._applyThreadRename();
}
+ if (this.isEditingThreadDescription && this.threadDescriptionInputRef.el && !this.threadDescriptionInputRef.el.contains(ev.target)) {
+ this._applyThreadChangeDescription();
+ }
}
}
@@ -217,6 +330,10 @@ function factory(dependencies) {
*/
doFocusOnThreadNameInput: attr(),
/**
+ * Determines whether this thread description input needs to be focused.
+ */
+ doFocusOnThreadDescriptionInput: attr(),
+ /**
* Determines the direction to set on the selection of this thread name
* input. This value is not a representation of current selection, but
* an instruction to set a new selection. Must be set together with
@@ -224,6 +341,14 @@ function factory(dependencies) {
* to have an effect.
*/
doSetSelectionDirectionOnThreadNameInput: attr(),
+ /**
+ * Determines the direction to set on the selection of this thread description
+ * input. This value is not a representation of current selection, but
+ * an instruction to set a new selection. Must be set together with
+ * `doSetSelectionEndOnThreadDescriptionInput` and `doSetSelectionStartOnThreadDescriptionInput`
+ * to have an effect.
+ */
+ doSetSelectionDirectionOnThreadDescriptionInput: attr(),
/**
* Determines the ending position where to place the selection on this
* thread name input (zero-based index). This value is not a
@@ -232,6 +357,14 @@ function factory(dependencies) {
* and `doSetSelectionStartOnThreadNameInput` to have an effect.
*/
doSetSelectionEndOnThreadNameInput: attr(),
+ /**
+ * Determines the ending position where to place the selection on this
+ * thread description input (zero-based index). This value is not a
+ * representation of current selection, but an instruction to set a new
+ * selection. Must be set together with `doSetSelectionDirectionOnThreadDescriptionInput`
+ * and `doSetSelectionStartOnThreadDescriptionInput` to have an effect.
+ */
+ doSetSelectionEndOnThreadDescriptionInput: attr(),
/**
* Determines the starting position where to place the selection on this
* thread name input (zero-based index). This value is not a
@@ -240,12 +373,26 @@ function factory(dependencies) {
* `doSetSelectionEndOnThreadNameInput` to have an effect.
*/
doSetSelectionStartOnThreadNameInput: attr(),
+ /**
+ * Determines the starting position where to place the selection on this
+ * thread description input (zero-based index). This value is not a
+ * representation of current selection, but an instruction to set a new
+ * selection. Must be set together with `doSetSelectionDirectionOnThreadDescriptionInput` and
+ * `doSetSelectionEndOnThreadDescriptionInput` to have an effect.
+ */
+ doSetSelectionStartOnThreadDescriptionInput: attr(),
/**
* Determines whether this thread is currently being renamed.
*/
isEditingThreadName: attr({
default: false,
}),
+ /**
+ * Determines whether this thread description is currently being changed.
+ */
+ isEditingThreadDescription: attr({
+ default: false,
+ }),
/**
* States whether the cursor is currently over this thread name in
* the top bar.
@@ -253,6 +400,13 @@ function factory(dependencies) {
isMouseOverThreadName: attr({
default: false,
}),
+ /**
+ * States whether the cursor is currently over this thread description in
+ * the top bar.
+ */
+ isMouseOverThreadDescription: attr({
+ default: false,
+ }),
/**
* Determines the pending name of this thread, which is the new name of
* the thread as the current user is currently typing it, with the goal
@@ -262,6 +416,15 @@ function factory(dependencies) {
pendingThreadName: attr({
default: "",
}),
+ /**
+ * Determines the pending description of this thread, which is the new description of
+ * the thread as the current user is currently typing it, with the goal
+ * of changing the description the thread.
+ * This value can either be applied or discarded.
+ */
+ pendingThreadDescription: attr({
+ default: "",
+ }),
/**
* States the thread that is displayed by this top bar.
*/
@@ -273,6 +436,11 @@ function factory(dependencies) {
* Useful to focus it, or to know when a click is done outside of it.
*/
threadNameInputRef: attr(),
+ /**
+ * States the OWL ref of the thread description input of this top bar.
+ * Useful to focus it, or to know when a click is done outside of it.
+ */
+ threadDescriptionInputRef: attr(),
/**
* States the thread view managing this top bar.
*/