[FIX] mail: users can now reply directly in documents from channels

A missing feature in discuss was the possibility to directly reply to a
message from a channel without opening the document.  This commit add
that feature.  Now, there is a small 'reply' icon in channels (including
inbox/starred) if the message comes from another document.  This icon
opens an extended composer, which will post the message in the document
chatter.
This commit is contained in:
Géry Debongnie
2016-02-03 14:36:30 +01:00
parent cc36e03ddc
commit 90d4f76100
9 changed files with 145 additions and 36 deletions
@@ -694,6 +694,9 @@ var chat_manager = {
}
},
get_message: function (id) {
return _.findWhere(messages, {id: id});
},
get_messages: function (options) {
var channel;
+1
View File
@@ -29,6 +29,7 @@ return Widget.extend({
this.folded = is_folded;
this.options = _.defaults(options || {}, {
display_stars: true,
display_reply_icon: false,
placeholder: _t("Say something"),
input_less: false,
});
+64 -10
View File
@@ -116,7 +116,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
chat_manager.undo_mark_as_read(msgs_ids, channel);
});
},
"click .o_mail_annoying_notification_bar .fa-close": function (event) {
"click .o_mail_annoying_notification_bar .fa-close": function () {
this.$(".o_mail_annoying_notification_bar").slideUp();
},
"click .o_mail_request_permission": function (event) {
@@ -124,6 +124,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.$(".o_mail_annoying_notification_bar").slideUp();
window.Notification.requestPermission();
},
"keydown": function (event) {
if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
this.unselect_message();
}
},
},
on_attach_callback: function () {
@@ -144,6 +149,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.channels_scrolltop = {};
this.throttled_render_sidebar = _.throttle(this.render_sidebar.bind(this), 100, { leading: false });
this.notification_bar = (window.Notification && window.Notification.permission === "default");
this.selected_message = null;
},
willStart: function () {
@@ -206,6 +212,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.thread.on('toggle_star_status', this, function (message_id) {
chat_manager.toggle_star_status(message_id);
});
this.thread.on('select_message', this, this.select_message);
this.thread.on('unselect_message', this, this.unselect_message);
this.basic_composer.on('post_message', this, this.on_post_message);
this.basic_composer.on('input_focused', this, this.on_composer_input_focused);
this.extended_composer.on('post_message', this, this.on_post_message);
@@ -241,6 +250,31 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
});
},
select_message: function(message_id) {
this.$el.addClass('o_mail_selection_mode');
var message = chat_manager.get_message(message_id);
this.selected_message = message;
var subject = "Re: " + message.record_name;
this.extended_composer.set_subject(subject);
if (this.channel.type !== 'static') {
this.basic_composer.toggle(false);
}
this.extended_composer.toggle(true);
this.thread.scroll_to({id: message_id, duration: 200, only_if_necessary: true});
this.extended_composer.focus('body');
},
unselect_message: function() {
if (this.channel.type !== 'static' && !this.channel.mass_mailing) {
this.basic_composer.toggle(true);
this.basic_composer.focus();
}
this.$el.removeClass('o_mail_selection_mode');
this.extended_composer.toggle(this.channel.mass_mailing);
this.thread.unselect();
this.selected_message = null;
},
render_sidebar: function () {
var self = this;
var $sidebar = $(QWeb.render("mail.chat.Sidebar", {
@@ -299,14 +333,16 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
});
},
render_snackbar: function (nb_needactions) {
this.$snackbar = $(QWeb.render('mail.chat.UndoSnackbar', {
nb_needactions: nb_needactions,
}));
render_snackbar: function (template, context, timeout) {
if (this.$snackbar) {
this.$snackbar.remove();
}
timeout = timeout || 20000;
this.$snackbar = $(QWeb.render(template, context));
this.$('.o_mail_chat_content').append(this.$snackbar);
// Hide snackbar after 20s
// Hide snackbar after [timeout] milliseconds (by default, 20s)
var $snackbar = this.$snackbar;
setTimeout(function() { $snackbar.fadeOut(); }, 20000);
setTimeout(function() { $snackbar.fadeOut(); }, timeout);
},
do_search_channel: function(search_val){
@@ -349,7 +385,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
if (channel.type !== 'static') {
// Display snackbar if needactions have been cleared
if (channel.needaction_counter > 0) {
self.render_snackbar(channel.needaction_counter);
self.render_snackbar('mail.chat.MessageSentSnackbar', {
nb_needactions: channel.needaction_counter,
});
}
chat_manager.mark_channel_as_seen(channel);
self.clear_needactions_def = chat_manager.mark_all_as_read(channel);
@@ -374,6 +412,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
self.basic_composer.toggle(channel.type !== 'static' && !channel.mass_mailing);
self.extended_composer.toggle(channel.type !== 'static' && channel.mass_mailing);
self.$el.removeClass('o_mail_selection_mode');
self.$('.o_mail_chat_channel_item')
.removeClass('o_active')
@@ -439,6 +478,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
display_empty_channel: !messages.length && !this.domain.length,
display_no_match: !messages.length && this.domain.length,
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
display_reply_icon: true,
};
},
@@ -512,11 +552,25 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
on_post_message: function (message) {
var self = this;
var options = {channel_id: this.channel.id};
var options = this.selected_message ? {} : {channel_id: this.channel.id};
if (this.selected_message) {
message.subtype = 'mail.mt_comment';
message.subtype_id = false;
message.message_type = 'comment';
message.content_subtype = 'html';
options.model = this.selected_message.model;
options.res_id = this.selected_message.res_id;
}
chat_manager
.post_message(message, options)
.then(function() {
self.thread.scroll_to();
if (self.selected_message) {
self.render_snackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000);
self.unselect_message();
} else {
self.thread.scroll_to();
}
})
.fail(function () {
// todo: display notification
+9 -3
View File
@@ -698,9 +698,15 @@ var ExtendedComposer = BasicComposer.extend({
should_send: function () {
return false;
},
focus: function () {
this.$subject_input.focus();
focus: function (target) {
if (target === 'body') {
this.$input.focus();
} else {
this.$subject_input.focus();
}
},
set_subject: function(subject) {
this.$('.o_composer_subject input').val(subject);
},
});
+40 -16
View File
@@ -21,27 +21,36 @@ var Thread = Widget.extend({
"click strong": "on_click_redirect",
"click .o_thread_show_more": "on_click_show_more",
"click .o_thread_message_needaction": function (event) {
event.stopPropagation();
var message_id = $(event.currentTarget).data('message-id');
this.trigger("mark_as_read", message_id);
},
"click .o_thread_message_star": function (event) {
event.stopPropagation();
var message_id = $(event.currentTarget).data('message-id');
this.trigger("toggle_star_status", message_id);
},
"click .o_thread_message": function (event) {
var selected = $(event.currentTarget).hasClass('o_thread_selected_message');
"click .o_thread_message_reply": function (event) {
this.selected_id = $(event.currentTarget).data('message-id');
this.$('.o_thread_message').removeClass('o_thread_selected_message');
$(event.currentTarget).toggleClass('o_thread_selected_message', !selected);
this.$('.o_thread_message[data-message-id=' + this.selected_id + ']')
.addClass('o_thread_selected_message');
this.trigger('select_message', this.selected_id);
event.stopPropagation();
},
"click .oe_mail_expand": function (event) {
event.preventDefault();
event.stopPropagation();
var $message = $(event.currentTarget).parents('.o_thread_message');
$message.addClass('o_message_expanded');
this.expanded_msg_ids.push($message.data('message-id'));
},
"click .o_thread_message": function (event) {
$(event.currentTarget).toggleClass('o_thread_selected_message');
},
"click": function () {
if (this.selected_id) {
this.unselect();
this.trigger('unselect_message');
}
},
},
init: function (parent, options) {
@@ -54,8 +63,10 @@ var Thread = Widget.extend({
display_avatar: true,
shorten_messages: true,
squash_close_messages: true,
display_reply_icon: false,
});
this.expanded_msg_ids = [];
this.selected_id = null;
},
render: function (messages, options) {
@@ -69,11 +80,14 @@ var Thread = Widget.extend({
// Hide avatar and info of a message if that message and the previous
// one are both comments wrote by the same author at the same minute
// and in the same document (users can now post message in documents
// directly from a channel that follows it)
var prev_msg;
_.each(msgs, function (msg) {
if (!prev_msg || (Math.abs(msg.date.diff(prev_msg.date)) > 60000) ||
prev_msg.message_type !== 'comment' || msg.message_type !== 'comment' ||
(prev_msg.author_id[0] !== msg.author_id[0])) {
(prev_msg.author_id[0] !== msg.author_id[0]) || prev_msg.model !== msg.model ||
prev_msg.res_id !== msg.res_id) {
msg.display_author = true;
} else {
msg.display_author = !options.squash_close_messages;
@@ -138,6 +152,7 @@ var Thread = Widget.extend({
}
msg.display_subject = message.subject && !message.is_system_notification;
msg.is_selected = msg.id === this.selected_id;
return msg;
},
@@ -159,18 +174,23 @@ var Thread = Widget.extend({
/**
* Scrolls the thread to a given message or offset if any, to bottom otherwise
* @param {int} [target.id] optional: the id of the message to scroll to
* @param {int} [target.offset] optional: the number of pixels to scroll
* @param {int} [options.id] optional: the id of the message to scroll to
* @param {int} [options.offset] optional: the number of pixels to scroll
*/
scroll_to: function (target) {
target = target || {};
if (target.id !== undefined) {
var $target = this.$('.o_thread_message[data-message-id=' + target.id + ']');
if ($target.length) {
scroll_to: function (options) {
options = options || {};
if (options.id !== undefined) {
var $target = this.$('.o_thread_message[data-message-id=' + options.id + ']');
if (options.only_if_necessary) {
var delta = $target.parent().height() - $target.height();
var offset = delta < 0 ? 0 : delta - ($target.offset().top - $target.offsetParent().offset().top);
offset = - Math.min(offset, 0);
this.$el.scrollTo("+=" + offset + "px", options);
} else if ($target.length) {
this.$el.scrollTo($target);
}
} else if (target.offset !== undefined) {
this.$el.scrollTop(target.offset);
} else if (options.offset !== undefined) {
this.$el.scrollTop(options.offset);
} else {
this.$el.scrollTop(this.el.scrollHeight);
}
@@ -181,6 +201,10 @@ var Thread = Widget.extend({
is_at_bottom: function () {
return this.el.scrollHeight - this.$el.scrollTop() - this.$el.outerHeight() < 5;
},
unselect: function () {
this.$('.o_thread_message').removeClass('o_thread_selected_message');
this.selected_id = null;
},
});
Thread.ORDER = ORDER;
+12 -2
View File
@@ -120,8 +120,7 @@
.o_mail_thread {
.o-flex(1, 0, 0);
overflow: auto;
.o-webclient-padding();
padding-bottom: 15px;
padding: 0 0 15px 0;
.o_chat_help_message {
padding: 25px 0;
@@ -253,6 +252,17 @@
}
}
}
&.o_mail_selection_mode {
.o_thread_message {
opacity: 0.5;
&.o_thread_selected_message {
padding: 4px @odoo-horizontal-padding;
opacity: 1;
background-color: lightgray;
}
}
}
}
// User status bullet (in Discuss sidebar and Invite dialog)
+5 -4
View File
@@ -40,7 +40,8 @@
.o_thread_message {
.o-flex-display();
padding: 4px 0;
padding: 4px @odoo-horizontal-padding;
margin-bottom: 0px;
.o_thread_message_sidebar {
.o-flex(0, 0, @mail-thread-avatar-size);
@@ -69,7 +70,7 @@
}
}
i.o_thread_message_star, i.o_thread_message_needaction {
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply {
cursor: pointer;
opacity: 0;
&.fa-star {
@@ -83,7 +84,7 @@
opacity: @mail-thread-side-date-opacity;
}
i.o_thread_message_star, i.o_thread_message_needaction {
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply {
opacity: @mail-thread-icon-opacity;
&:hover {
opacity: 1;
@@ -141,7 +142,7 @@
}
}
.o_thread_message_star, .o_thread_message_needaction {
.o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply {
padding: 4px;
}
}
@@ -118,6 +118,13 @@
</div>
</t>
<t t-name="mail.chat.MessageSentSnackbar">
<div class="alert o_mail_snackbar" data-dismiss="alert" role="alert">
Message sent in "<t t-esc="record_name"/>".
</div>
</t>
<!-- Buttons of the Control Panel -->
<t t-name="mail.chat.ControlButtons">
<div>
+4 -1
View File
@@ -46,7 +46,7 @@
</t>
<t t-name="mail.ChatThread.Message">
<div t-att-class="'o_thread_message' + (message.expanded ? ' o_message_expanded' : '')" t-att-data-message-id="message.id">
<div t-att-class="'o_thread_message' + (message.expanded ? ' o_message_expanded ' : ' ') + (message.is_selected ? 'o_thread_selected_message' : '')" t-att-data-message-id="message.id">
<div t-if="options.display_avatar" class="o_thread_message_sidebar">
<t t-if="!message.mailto &amp;&amp; message.author_id[0]">
<img t-if="message.avatar_src &amp;&amp; message.display_author"
@@ -103,6 +103,9 @@
<i t-if="message.is_needaction &amp;&amp; options.display_needactions"
class="fa fa-check o_thread_message_needaction"
t-att-data-message-id="message.id" title="Mark as Read"/>
<i t-if="message.record_name &amp;&amp; message.model != 'mail.channel' &amp;&amp; options.display_reply_icon"
class="fa fa-reply o_thread_message_reply"
t-att-data-message-id="message.id" title="Reply"/>
</span>
</p>
<t t-if="message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0">