summaryrefslogtreecommitdiff
path: root/addons/mail/static/src
diff options
context:
space:
mode:
authorJérome Maes <jem@openerp.com>2015-08-11 10:17:10 +0200
committerAaron Bohy <aab@odoo.com>2015-09-01 20:16:09 +0200
commit449f25463843767ba41a9a8c2adfdd4ceb2f2e5c (patch)
tree5a45357150f4b6f1d7977badc9c84843f64d8d11 /addons/mail/static/src
parenta9a78ee9b6a499c29b284e1612951cc20249502f (diff)
[REF] mail : include real time, and replace Timeline features
This commit add instant messaging features in mail module to make it a real team collaboration tool. Lots of these new features will replace Timeleine view (which is not removed in this commit). Many files have been moved, split or created for a better structure. - My Channels : A user can be member of channel (mail.channel). These are discussion group, but also an aggregate of notification. Setting a channel as document follower, it will receive all the message (of the chatter) in real time, as information stream. - A new Inbox : this aims to replace the timeline view. The user will see its channels grouped by 'slot', and receive the message in real time. Jump to the form view, invite people, create private discussion group, talk directly to another employee, ... are the main features. Your message can still be starred. The 'Inbox' will only contain your needaction. - Needaction concept : a needaction is a message calling you to do something. If you follow a document as a person, the posted message will be 'needaction' for you. It can also be a message where you are mention with a '@my_name' (this feature is not added in this commit). - The chatter doesn't change so much. It still allow you to see the discussion about the document, to star some message, and treat your needaction. - Chat : the only way to have a minified conversation is from the Inbox Client Action. It allow you to keep an eye on a channel when working on document. - Compose Message : it is now common to the Inbox, and the chatter. It offers shortcode substitution, attachments management, and optional subtype. - UI : the chat window has been clean, they have a new look now. To do so, - Chatter code have been completely rewrite, independently of timeline view - Add 'bus' as mail dependency, to insure real time - A mail.message is broadcasted on the bus channel (db, mail.channel, channel_id), and a channel header on (db, res.partner, partner_id) - Extracting mail_thread mixin used for Chatter and ChatMailThread (Inbox client action). This uniformize the programming structure with the server side - Clean CSS style and use LESS instead of CSS - Define a new messafe format (message_format method in mail_message.py), compatible with fetching and broadcasting - Apply guideline coding conventions This commit also break the module im_livechat, since im_chat models don't exists anymore and will be fixed in the next commit. Thanks to Thibault Delavallee (tde) for the long debates and precious advices, Richard Mathot (rim) for the support, Simon Lejeune (sle) for the client action layout, and the Usability Team (apr, lle, bst yti) for the long testing. Conflicts: addons/mail/models/mail_thread.py
Diffstat (limited to 'addons/mail/static/src')
-rw-r--r--addons/mail/static/src/css/mail.css102
-rw-r--r--addons/mail/static/src/css/mail_channel.css29
-rw-r--r--addons/mail/static/src/css/mail_chat_backend.css144
-rw-r--r--addons/mail/static/src/css/mail_chat_common.css270
-rw-r--r--addons/mail/static/src/js/mail.js8
-rw-r--r--addons/mail/static/src/js/mail_chat_backend.js1177
-rw-r--r--addons/mail/static/src/js/mail_chat_common.js427
-rw-r--r--addons/mail/static/src/js/mail_chatter.js471
-rw-r--r--addons/mail/static/src/js/mail_followers.js30
-rw-r--r--addons/mail/static/src/js/mail_thread.js430
-rw-r--r--addons/mail/static/src/js/mail_utils.js105
-rw-r--r--addons/mail/static/src/less/mail_chat_backend.less212
-rw-r--r--addons/mail/static/src/less/mail_chatter.less54
-rw-r--r--addons/mail/static/src/less/mail_thread.less184
-rw-r--r--addons/mail/static/src/xml/mail_chat_backend.xml155
-rw-r--r--addons/mail/static/src/xml/mail_chat_common.xml173
-rw-r--r--addons/mail/static/src/xml/mail_chatter.xml123
-rw-r--r--addons/mail/static/src/xml/mail_followers.xml98
-rw-r--r--addons/mail/static/src/xml/mail_thread.xml186
19 files changed, 3378 insertions, 1000 deletions
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index 2d14d8f98b0..522495a8fac 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -4,7 +4,7 @@
position: relative;
color: white;
}
-
+
/* ----------------------- TIMELINE ----------------------- */
@@ -23,7 +23,7 @@
.o_web_client .o_timeline.o_timeline_inbox {
padding-top: 15px;
- padding-bottom: 15px;
+ padding-bottom: 15px;
}
.o_web_client .o_timeline.o_timeline_inbox .oe_thread {
@@ -117,7 +117,7 @@
}
.o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject .o_timeline_nb_unread_msg {
- font-weight: bold;
+ font-weight: bold;
}
.o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_author {
@@ -140,10 +140,10 @@
/* avatar */
- .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar {
- left:0;
- top: 0;
- bottom: 0;
+ .o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar {
+ left:0;
+ top: 0;
+ bottom: 0;
width: 30px;
overflow: hidden;
padding-right: 3px;
@@ -188,7 +188,7 @@
.o_web_client .o_timeline .o_timeline_thread_parent .o_timeline_parent_followers {
display: inline-block;
min-width: 45px;
- margin-right: 10px;
+ margin-right: 10px;
color: #337ab7;
}
@@ -337,7 +337,7 @@
.o_web_client .o_timeline button.oe_full,
.o_web_client .o_timeline button.oe_attach {
- background: transparent;
+ background: transparent;
color: #337ab7;
box-shadow: none;
border: none;
@@ -456,22 +456,22 @@
@-webkit-keyframes oe_mail_attach_loading_anim {
0% { background: #4BBD00 }
- 50% { background: #009123 }
+ 50% { background: #009123 }
100% { background: #4BBD00 }
}
@-moz-keyframes oe_mail_attach_loading_anim {
0% { background: #4BBD00 }
- 50% { background: #009123 }
+ 50% { background: #009123 }
100% { background: #4BBD00 }
}
@-o-keyframes oe_mail_attach_loading_anim {
0% { background: #4BBD00 }
- 50% { background: #009123 }
+ 50% { background: #009123 }
100% { background: #4BBD00 }
}
@keyframes oe_mail_attach_loading_anim {
0% { background: #4BBD00 }
- 50% { background: #009123 }
+ 50% { background: #009123 }
100% { background: #4BBD00 }
}
@@ -482,29 +482,29 @@
left: 0px;
right: 0px;
max-height: 64px;
- background: rgba(0,0,0,0.8);
+ background: rgba(0,0,0,0.8);
color: white;
border-radius: 1px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
opacity: 0;
- -webkit-transition: opacity 0.2s linear;
- -moz-transition: opacity 0.2s linear;
- -o-transition: opacity 0.2s linear;
- transition: opacity 0.2s linear;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
}
.oe_attachment.oe_preview:hover .oe_name {
opacity: 1;
- -webkit-transition: opacity 0.2s linear;
- -moz-transition: opacity 0.2s linear;
- -o-transition: opacity 0.2s linear;
- transition: opacity 0.2s linear;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
}
.o_web_client .o_timeline .o_timeline_msg_footer button.oe_attach {
width: 24px;
- margin-bottom: 5px;
+ margin-bottom: 5px;
filter: none;
}
@@ -521,7 +521,7 @@
cursor: pointer;
}
- .o_web_client .o_timeline .o_timeline_msg_footer .oe_attachment_file .oe_form_binary_file {
+ .o_web_client .o_timeline .o_timeline_msg_footer .oe_attachment_file .oe_form_binary_file {
display: inline;
position: relative;
margin-left: -100px;
@@ -538,7 +538,7 @@
vertical-align: middle;
}
- .o_web_client .o_timeline .oe_hidden_input_file,
+ .o_web_client .o_timeline .oe_hidden_input_file,
.o_web_client .o_timeline .oe_hidden_input_file form {
display: inline;
cursor: pointer;
@@ -557,26 +557,26 @@
text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white;
cursor: pointer;
opacity: 0;
- -webkit-transition: opacity 0.2s linear;
- -moz-transition: opacity 0.2s linear;
- -o-transition: opacity 0.2s linear;
- transition: opacity 0.2s linear;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
}
.o_web_client .o_timeline .o_timeline_msg_composer .oe_attachment:hover .oe_delete .fa {
opacity: 1;
- -webkit-transition: opacity 0.2s linear;
- -moz-transition: opacity 0.2s linear;
- -o-transition: opacity 0.2s linear;
- transition: opacity 0.2s linear;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
}
- /* ------------ expandable message ------------ */
+ /* ------------ expandable message ------------ */
- .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_expandable,
- .o_web_client .o_timeline .o_timeline_parent_expandable {
+ .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_expandable,
+ .o_web_client .o_timeline .o_timeline_parent_expandable {
color: #A4A4A4;
- cursor: pointer;
+ cursor: pointer;
margin-bottom: 0px;
min-height: 30px;
height: 30px;
@@ -637,10 +637,10 @@
/* 2. avatar (message_left) */
.o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_left {
- float: left;
- left:0;
- top: 0;
- bottom: 0;
+ float: left;
+ left:0;
+ top: 0;
+ bottom: 0;
width: 45px;
overflow: hidden;
}
@@ -677,11 +677,11 @@
-ms-border-bottom-right-radius: 4px;
-o-border-bottom-right-radius: 4px;
border-bottom-right-radius: 4px;
- position: relative;
- background: white;
- margin-left: 53px;
- padding: 5px 5px 11px 7px;
- box-shadow: 0px 1px #CACACA;
+ position: relative;
+ background: white;
+ margin-left: 53px;
+ padding: 5px 5px 11px 7px;
+ box-shadow: 0px 1px #CACACA;
}
.o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_comment .o_timeline_msg_center:after, /*********/
@@ -704,7 +704,7 @@
background-color: #DADADA;
}
.o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_comment.o_timeline_msg_nobody .o_timeline_msg_center:after,
- .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_center:after {
+ .o_web_client .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_center:after {
border-right-color: #DADADA;
}
@@ -717,7 +717,7 @@
.o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header {
font-weight: normal;
- margin-bottom: 11px;
+ margin-bottom: 11px;
}
.o_web_client .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header a {
@@ -733,7 +733,7 @@
margin-right: 5px;
}
- /* message header - votes */
+ /* message header - votes */
.o_web_client .o_timeline .o_timeline_vote {
display: inline-block;
@@ -927,8 +927,8 @@
text-overflow: ellipsis;
}
- .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_follow,
- .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_unfollow,
+ .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_follow,
+ .o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_unfollow,
.o_web_client .o_timeline_followers button.o_timeline_follower .o_timeline_following {
display: none;
}
diff --git a/addons/mail/static/src/css/mail_channel.css b/addons/mail/static/src/css/mail_channel.css
index 51b04a4a9fc..923cdffe726 100644
--- a/addons/mail/static/src/css/mail_channel.css
+++ b/addons/mail/static/src/css/mail_channel.css
@@ -1,32 +1,3 @@
-/* ------------------------------ */
-/* Group Form */
-/* ------------------------------ */
-
-/* Resize sheet width */
-.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet {
- min-height: 0px;
- max-height: none;
-}
-
-.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_sheet {
- min-height: 0px;
- max-height: none;
- padding: 0px 18px;
-}
-
-.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_avatar {
- padding-top: 3px;
-}
-
-/* Resize footer width */
-.o_web_client .oe_form div.oe_mail_channel_footer {
- max-width: 80%;
-}
-
-/* Resize group description */
-.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_field_text > textarea {
- height: 60px;
-}
/* ------------------------------ */
/* Group Kanban */
diff --git a/addons/mail/static/src/css/mail_chat_backend.css b/addons/mail/static/src/css/mail_chat_backend.css
index d01577eb56a..d896befbf5e 100644
--- a/addons/mail/static/src/css/mail_chat_backend.css
+++ b/addons/mail/static/src/css/mail_chat_backend.css
@@ -1,12 +1,10 @@
+
.oe_im {
position: fixed;
- background-color: #eaeaea;
- width: 250px;
+ background-color: #E8EBEF;
+ width: 220px;
border-left: 1px solid #AEB9BD;
z-index: 500;
- -webkit-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
- -moz-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
- box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
}
@media print {
@@ -23,92 +21,66 @@
top: -8px;
}
-/* search stuff */
-.oe_im_frame_header {
- position: relative;
- padding: 16px;
-}
-.oe_im_frame_header .oe_im_searchbox {
- width: 200px;
- padding: 1px 21px 1px 19px;
- font-size: 13px;
- border: none;
- border-bottom: 1px solid #c6c6c6;
- background: none;
- margin-left: 20px;
- height: 38px;
-}
-.oe_im_frame_header .oe_im_searchbox:focus {
- outline: none;
-}
-.oe_im_frame_header .oe_im_search_icon {
- position: absolute;
- color: #888;
- top: 28px;
- left: 8px;
- font-size: 16px;
- font-weight: 300 !important;
-}
-.oe_im_frame_header .oe_im_search_clear {
- display: none;
- position: absolute;
- right: 11px;
- top: 4px;
- font-size: 26px;
- color: #b6b6b6;
- cursor: pointer;
-}
-.oe_im_frame_header .oe_im_search_clear:hover {
- color: #888;
-}
-/* users */
-.oe_im_users {
- padding-bottom: 38px;
+
+/*
+INSTANT MESSAGING VIEW
+*/
+.o_mail_chat_im_container{
+ margin: 0;
height: 100%;
+}
+.o_mail_chat_im_row{
+ height: inherit;
+}
+.o_mail_chat_im_container .o_mail_chat_im_sidebar{
+ background-color: #f5f5f5;
+ height: inherit;
overflow-y: scroll;
}
-.oe_im_user {
- position: relative;
- padding: 2px 8px;
- cursor: pointer;
- font-size: 13px;
- margin-bottom: 10px;
-}
-.oe_im_user:hover {
- background: lightGrey;
-}
-.oe_im_user_clip {
- display: inline-block;
- width: 26px;
- height: 26px;
- margin-right: 4px;
- -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
- -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
- box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
-}
-.oe_im_user_avatar {
- width: 26px;
- height: auto;
-}
-.oe_im_user_name {
- width: 162px;
- line-height: 26px;
- padding-right: 15px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- position: relative;
+
+.o_mail_chat_im_container .o_mail_chat_im_content{
+ height: inherit;
+}
+/* content of conversation */
+.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation{
+ height: inherit;
+}
+.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation .o_mail_chat_im_messages{
+ height: 80%;
+ overflow-y: scroll;
+}
+.o_mail_chat_im_container .o_mail_chat_im_content_wrap_conversation .o_mail_compose_message{
+ height: 20%;
+ padding:10px;
+ background-color: #f5f5f5;
+}
+.o_mail_chat_im_container .o_mail_chat_im_messages .o_mail_chat_im_message{
+ margin: 0;
+}
+/* content of personal inbox and starred */
+.o_mail_chat_im_container .o_mail_chat_im_content_wrap_personal{
+ height: inherit;
+ overflow-y: scroll;
}
-.oe_im_user_online {
- display: none;
- position: absolute;
- top: 9.5px;
- right: 11px;
- width: 11px;
- height: 11px;
- vertical-align: middle;
- border: 0;
+/* message row */
+.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star{
+ opacity: 0.5;
+ color: black;
}
+.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star:hover{
+ opacity: 1;
+ cursor: pointer;
+}
+.o_mail_chat_im_container .o_mail_chat_im_message .o_mail_chat_im_star.o_mail_starred{
+ color: orange;
+}
+
+
+
+
+.o_mail_chat_im_search_partner{
+ height: 50px;
+} \ No newline at end of file
diff --git a/addons/mail/static/src/css/mail_chat_common.css b/addons/mail/static/src/css/mail_chat_common.css
index 1ad2981eb41..97119ed506c 100644
--- a/addons/mail/static/src/css/mail_chat_common.css
+++ b/addons/mail/static/src/css/mail_chat_common.css
@@ -1,92 +1,51 @@
-
-
-.openerp_style { /* base style of openerp */
- font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
- color: #4c4c4c;
- font-size: 13px;
- background: white;
-}
-
-/* button */
-
-.oe_chat_button {
- position: fixed;
- bottom: 0px;
- right: 6px;
- display: inline-block;
- min-width: 100px;
- background-color: rgba(60, 60, 60, 0.6);
- font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif;
- font-size: 14px;
- font-weight: bold;
- padding: 10px;
- color: white;
- text-shadow: rgb(59, 76, 88) 1px 1px 0px;
- border: 1px solid rgb(80, 80, 80);
- border-bottom: 0px;
- border-top-left-radius: 5px;
- border-top-right-radius: 5px;
- cursor: pointer;
- z-index:500;
+/* emoji size */
+.o_mail_emoji img{
+ height: 20px;
}
-/* conversations */
+// ------------------------------------------------------------------
+// Chat Window
+// ------------------------------------------------------------------
-.oe_im_chatview {
- position: fixed;
- overflow: hidden;
- margin-bottom: 5px;
- margin-right: 6px;
- background-color: #fff;
- border: 1px solid #DEDEDE;
- width: 240px;
- z-index:500;
-}
@media screen and (max-width:360px) {
- .oe_im_chatview {
+ .o_mail_chat_im_window {
width: 100%;
margin-right: 0;
}
- .oe_im_chatview .oe_im_chatview_content {
+ .o_mail_chat_im_window .o_mail_chat_im_window_content {
width: 100% !important;
}
- .oe_im_chatview .oe_im_chatview_input {
+ .o_mail_chat_im_window .o_mail_chat_im_window_input {
width: 100% !important;
}
}
-.oe_im_chatview .oe_im_chatview_disconnected {
- display:none;
- z-index: 100;
- width: 100%;
- background: #E8EBEF;
- padding: 5px;
- font-size: 11px;
- color: #999;
- line-height: 14px;
- height: 28px;
- overflow: hidden;
-}
-.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected {
- display: block;
-}
+.o_mail_chat_im_window {
+ position: fixed;
+ overflow: hidden;
+ margin-bottom: 5px;
+ margin-right: 6px;
+ background: rgba(250, 250, 250, 0.95);
+ border: 1px solid #DEDEDE;
+ width: 240px;
+ z-index:500;
+}
-/* ####### Chat View : Header ####### */
-.oe_im_chatview_header {
+/* HEADER */
+.o_mail_chat_im_window .o_mail_chat_im_window_header {
padding: 3px 6px 2px;
+ cursor: pointer;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
-webkit-border-radius: 3px 3px 0 0;
- border-bottom: 1px solid #AEB9BD;
- cursor: pointer;
}
-.oe_im_chatview_header .header_name{
+.o_mail_chat_im_window .o_mail_chat_im_window_header_name {
max-width: 75%;
word-wrap: break-word;
display: inline-block;
@@ -96,164 +55,61 @@
text-overflow: ellipsis;
}
-.oe_im_chatview_header .right_buttons {
+.o_mail_chat_im_window .o_mail_chat_im_window_header_buttons{
float: right;
}
-.oe_im_chatview_header .right_buttons > * {
- display: inline-block;
- border: 0;
- cursor: pointer;
- color: gray ;
- background: transparent;
- font-size: 16px;
- line-height: 16px;
- margin-left: 5px;
- -webkit-appearance: none;
-}
-
-.oe_im_chatview_header .right_buttons .dropdown-menu{
- right: 0;
- left: auto;
-}
-.oe_im_chatview_header .right_buttons .dropdown-menu > li{
- display: block;
- padding: 3px 20px;
- clear: both;
- font-weight: normal;
- line-height: 1.428571429;
- color: #4c4c4c;
- white-space: nowrap;
-}
-
-.oe_im_chatview .oe_im_chatview_content {
- overflow: auto;
- height: 287px;
- width: 240px;
-}
-.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content {
- height: 249px;
+/* CONTENT (messages, ...) */
+.o_mail_chat_im_window .o_mail_chat_im_window_content {
+ height: 280px;
+ overflow-y: scroll;
}
-.oe_im_chatview .oe_im_chatview_footer {
- position: relative;
- padding: 3px;
- border-top: 1px solid #AEB9BD;
- background: #DEDEDE;
- background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
- background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
- -moz-border-radius: 0 0 3px 3px;
- -webkit-border-radius: 0 0 3px 3px;
- border-radius: 0 0 3px 3px;
+
+.o_mail_chat_im_window .o_mail_chat_im_messages .o_mail_chat_im_window_date_sperator{
+ padding: 5px;
+ padding-right: 10px;
+ text-align: right;
+ font-weight: bold;
}
-.oe_im_chatview .oe_im_chatview_input {
- width: 222px;
- font-family: Lato, Helvetica, sans-serif;
- font-size: 13px;
- color: #333;
- padding: 1px 5px;
- border: 1px solid #AEB9BD;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- -moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2);
- -webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
- box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
+
+.o_mail_chat_im_window .o_mail_chat_im_message{
+ margin: auto;
+ margin-bottom: 5px;
}
-.oe_im_chatview .oe_im_chatview_message_bubble {
- background: white;
- position: relative;
- min-height: 32px;
- padding: 3px;
- margin: 3px;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
+
+.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar{
+ width: 20%;
+ display: inline-block;
+ vertical-align: top;
}
-/* Smiley size */
-.oe_im_chatview .oe_im_chatview_bubble_item img{
- width: 18px;
+.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_avatar img{
+ width: 80%;
+ float: right;
}
-.oe_im_chatview .oe_im_chatview_technical_bubble {
- background: #D8D8D8;
- position: relative;
- min-height: 32px;
- padding: 3px;
- margin: 3px;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
+.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core{
+ width: 75%;
+ display: inline-block;
+ padding: 5px;
- font-style:italic;
+ background: white;
+ border-radius: 2px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+ max-width:100%;
}
-.oe_im_chatview .oe_im_chatview_clip {
- position: relative;
- float: left;
- width: 26px;
- height: 26px;
- margin-right: 4px;
- -moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
- -webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
- box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
-}
-.oe_im_chatview .oe_im_chatview_avatar {
- float: left;
- width: 26px;
- height: auto;
- clip: rect(0, 26px, 26px, 0);
- max-width: 100%;
- width: auto 9;
- height: auto;
- vertical-align: middle;
- border: 0;
- -ms-interpolation-mode: bicubic;
-}
-.oe_im_chatview .oe_im_chatview_time {
- position: absolute;
- right: 0px;
- top: 0px;
- margin: 3px;
- text-align: right;
- line-height: 13px;
- font-size: 11px;
- color: #999;
- width: 60px;
- overflow: hidden;
-}
-.oe_im_chatview .oe_im_chatview_from {
- margin: 0 0 2px 0;
- line-height: 14px;
- font-weight: bold;
- font-size: 12px;
- width: 140px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- color: #3A87AD;
-}
-.oe_im_chatview .oe_im_chatview_bubble_list {
-}
-.oe_im_chatview .oe_im_chatview_bubble_item {
- margin: 0 0 2px 30px;
- line-height: 14px;
- word-wrap: break-word;
+.o_mail_chat_im_window .o_mail_chat_im_message .o_mail_chat_im_message_core p{
+ margin: 0;
}
-.oe_im_chatview_online {
- margin-top: -4px;
- width: 11px;
- height: 11px;
+/* FOOTER */
+.o_mail_chat_im_window .o_mail_chat_im_window_footer {
+ padding:3px;
+ background: #DEDEDE;
}
-.oe_im_chatview .oe_im_chatview_date_separator {
- background: transparent;
- position: relative;
- padding: 3px;
- color: #aaa;
- margin: 3px;
- text-align: right;
- font-size: 12px;
-} \ No newline at end of file
+.o_mail_chat_im_window .o_mail_chat_im_window_footer .o_mail_chat_im_window_input {
+ width: 100%;
+}
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index 9dd4b2c508d..8064fca4ee1 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -1167,7 +1167,7 @@ var ComposeMessage = Attachment.extend ({
var names_to_find = _.pluck(recipients_to_find, 'full_name');
var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
-
+
var names_to_remove = [];
var recipient_ids_to_remove = [];
@@ -1236,7 +1236,7 @@ var ComposeMessage = Attachment.extend ({
});
});
});
-
+
return check_done;
},
@@ -1445,10 +1445,10 @@ var ComposeMessageTopButton = Widget.extend({
});
// Put the ComposeMessageTopButton widget in the systray menu
-SystrayMenu.Items.push(ComposeMessageTopButton);
+//SystrayMenu.Items.push(ComposeMessageTopButton);
core.view_registry.add('timeline', TimelineView);
-core.form_widget_registry.add('mail_thread', TimelineRecordThread);
+//core.form_widget_registry.add('mail_thread', TimelineRecordThread);
return {
TimelineView: TimelineView,
diff --git a/addons/mail/static/src/js/mail_chat_backend.js b/addons/mail/static/src/js/mail_chat_backend.js
index cd1cc00fdf2..9353afb68ab 100644
--- a/addons/mail/static/src/js/mail_chat_backend.js
+++ b/addons/mail/static/src/js/mail_chat_backend.js
@@ -1,301 +1,1032 @@
-odoo.define('im_chat.im_chat', function (require) {
+odoo.define('mail.chat_backend', function (require) {
"use strict";
-// to do: make this work in website
-
var bus = require('bus.bus');
var core = require('web.core');
var data = require('web.data');
-var Model = require('web.DataModel');
-var session = require('web.session');
+var Model = require('web.Model');
+var pyeval = require('web.pyeval');
var SystrayMenu = require('web.SystrayMenu');
+var Widget = require('web.Widget');
+var Dialog = require('web.Dialog');
+var ControlPanelMixin = require('web.ControlPanelMixin');
+var SearchView = require('web.SearchView');
+var WebClient = require('web.WebClient');
+
+var session = require('web.session');
var utils = require('web.utils');
var web_client = require('web.web_client');
-var Widget = require('web.Widget');
-var im_chat_common = require('im_chat.im_chat_common');
+
+var mail_utils = require('mail.utils');
+var mail_chat_common = require('mail.chat_common');
+var mail_thread = require('mail.thread');
var _t = core._t;
var QWeb = core.qweb;
-
-var USERS_LIMIT = 20;
+var internal_bus = core.bus;
-// ########## CONVERSATION extentions ###############
-
-im_chat_common.ConversationManager.include({
- _message_receive: function(message){
+/**
+ * Widget handeling the channels, in the backend
+ *
+ * Responsible to listen the bus and apply action with the received message. Add layer to coordinate the
+ * folded conversation and trigger event for the InstantMessagingView client action (using internal
+ * comminication bus). It is a component of the WebClient.
+ **/
+var ConversationManagerBackend = mail_chat_common.ConversationManager.extend({
+ _setup: function(init_data){
var self = this;
- var session_id = message.to_id[0];
- var conv = this.sessions[message.to_id[1]];
- if(!conv){
- // fetch the session, and init it with the message
- var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){
- conv = self.session_apply(session, {'force_open': true});
- conv.message_receive(message);
- });
- }else{
- this._super(message);
+ this._super.apply(this, arguments);
+ _.each(init_data['notifications'], function(n){
+ self.on_notification(n);
+ });
+ },
+ // window title
+ window_title_change: function() {
+ this._super.apply(this, arguments);
+ var title = undefined;
+ if (this.get("waiting_messages") !== 0) {
+ title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"));
}
+ web_client.set_title_part("im_messages", title);
},
-});
-
-im_chat_common.Conversation.include({
- // user actions
- prepare_action_menu: function(){
- this._super();
- this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode);
- this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session);
+ // sessions and messages
+ session_apply: function(active_session, options){
+ // for chat windows
+ if(active_session.is_minimized || (!active_session.is_minimized && active_session.state === 'closed')){
+ this._super.apply(this, arguments);
+ }
+ // for client action
+ if(!active_session.is_minimized){ // if not minimized,
+ internal_bus.trigger('mail_session_receive', active_session);
+ }
},
- action_shorcode: function(e){
- return web_client.action_manager.do_action({
- type: 'ir.actions.act_window',
- name : _t('Shortcode'),
- res_model: 'im_chat.shortcode',
- view_mode: 'tree,form',
- view_type: 'tree',
- views: [[false, 'list'], [false, 'form']],
- target: "new",
- limit: 80,
- flags: {
- action_buttons: true,
- pager: true,
- }
+ message_receive: function(message) {
+ var actual_channel_ids = _.map(_.keys(this.sessions), function(item){
+ return parseInt(item);
});
+ var message_channel_ids = message.channel_ids;
+ if(_.intersection(actual_channel_ids, message_channel_ids).length){
+ this._super.apply(this, arguments);
+ }
+ // broadcast the message to the NotificationButton and the InstantMessagingView
+ internal_bus.trigger('mail_message_receive', message);
+ // increment the needaction top counter
+ if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){
+ internal_bus.trigger('mail_needaction_new', 1);
+ }
},
- action_quit_session: function(e){
+});
+
+
+/**
+ * Widget Minimized Conversation
+ *
+ * Add layer of WebClient integration, and user fold state handling (comminication with server)
+ **/
+mail_chat_common.Conversation.include({
+ session_fold: function(state){
var self = this;
- var Session = new Model("im_chat.session");
- return Session.call("quit_user", [this.get("session").uuid]).then(function(res) {
- if(! res){
- self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave."));
- }
+ var args = arguments;
+ var super_call = this._super;
+ // broadcast the state changing
+ return new Model("mail.channel").call("channel_fold", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){
+ super_call.apply(self, args);
});
},
- // session
- session_update_state: function(state){
+});
+
+/**
+ * Widget : Patch for WebClient
+ *
+ * Create the conversation manager, and attach it to the web_client.
+ **/
+WebClient.include({
+ show_application: function(){
var self = this;
var args = arguments;
var super_call = this._super;
- // broadcast the state changing
- return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){
+ this.mail_conversation_manager = new ConversationManagerBackend(this);
+ this.mail_conversation_manager.start().then(function(){
super_call.apply(self, args);
+ self.mail_conversation_manager.bus.start_polling();
});
},
- // window title
- window_title_change: function() {
- this.super();
- var title = undefined;
- if (this.get("waiting_messages") !== 0) {
- title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))
- }
- web_client.set_title_part("im_messages", title);
+});
+
+
+/**
+ * Widget Top Menu Notification Counter
+ *
+ * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to
+ * increment (or not) the counter. On click, should redirect to the client action.
+ **/
+var NotificationTopButton = Widget.extend({
+ template:'mail.chat.NotificationTopButton',
+ events: {
+ "click": "on_click",
},
- // TODO : change this way
- add_user: function(user){
- return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]);
+ init: function(parent){
+ this._super.apply(this, arguments);
+ this.set('counter', 0);
+ },
+ willStart: function(){
+ var self = this;
+ return $.when(session.rpc('/mail/needaction'), this._super()).then(function(needaction_count){
+ self.set('counter', needaction_count);
+ });
+ },
+ start: function() {
+ var self = this;
+ this.on("change:counter", this, this.on_change_counter);
+ // events
+ internal_bus.on('mail_needaction_new', this, this.counter_increment);
+ internal_bus.on('mail_needaction_done', this, this.counter_decrement);
+ return this._super();
+ },
+ counter_increment: function(inc){
+ this.set('counter', this.get('counter')+inc);
+ },
+ counter_decrement: function(dec){
+ this.set('counter', this.get('counter')-dec);
+ },
+ on_change_counter: function() {
+ this.$('.fa-comment').html(this.get('counter') || '');
+ },
+ on_click: function(e){
+ e.preventDefault();
+ this.do_action({
+ type: 'ir.actions.client',
+ tag: 'mail.chat.instant_messaging',
+ params: {
+ 'default_active_id': 'channel_inbox',
+ },
+ }, {
+ clear_breadcrumbs: true,
+ });
},
});
+SystrayMenu.Items.push(NotificationTopButton);
-// ###### BACKEND : contact panel, top menu button #########
-
-var UserWidget = Widget.extend({
- template: "im_chat.UserWidget",
+/**
+ * Abstract Class to 'Add More/Search' Widget
+ *
+ * Inputbox using jQueryUI autocomplete to fetch selection, like a Many2One field (on form view)
+ * Used to create or pin a mail.channel or a res.partner on the InstantMessagingView
+ **/
+var AbstractAddMoreSearch = Widget.extend({
+ template: 'mail.chat.AbstractAddMoreSearch',
events: {
- "click": "activate_user",
+ "click .o_mail_chat_add_more_text": "on_click_text",
+ "blur .o_mail_chat_search_input": "_toggle_elements",
},
- init: function(parent, user) {
- this._super(parent);
- this.set("id", user.id);
- this.set("name", user.name);
- this.set("im_status", user.im_status);
- this.set("image_url", user.image_url);
+ init: function(parent, options){
+ this._super.apply(this, arguments);
+ options = _.defaults(options || {}, {
+ 'can_create': false,
+ 'label': _t('+ Add More'),
+ });
+ this.limit = 10;
+ this.can_create = options.can_create;
+ this.label = options.label;
},
- start: function() {
- this.$el.data("user", {id:this.get("id"), name:this.get("name")});
- this.$el.draggable({helper: "clone"});
- this.on("change:im_status", this, this.update_status);
- this.update_status();
+ start: function(){
+ var self = this;
+ this.last_search_val = false;
+ this.$input = this.$('.o_mail_chat_search_input');
+ this._bind_events();
+ return this._super();
},
- update_status: function(){
- this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline');
- var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png');
- this.$(".oe_im_user_online").attr('src', img_src);
+ _bind_events: function(){
+ // autocomplete
+ var self = this;
+ this.$input.autocomplete({
+ source: function(request, response) {
+ self.last_search_val = request.term;
+ self.do_search(request.term).done(function(result){
+ if(self.can_create){
+ result.push({
+ 'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"'+self.last_search_val+'"</em>'),
+ 'value': '_create',
+ });
+ }
+ response(result);
+ });
+ },
+ select: function(event, ui) {
+ self.on_click_item(ui.item);
+ },
+ focus: function(event, ui) {
+ event.preventDefault();
+ },
+ html: true,
+ });
+ },
+ // ui
+ _toggle_elements: function(){
+ this.$('.o_mail_chat_add_more_text').toggle();
+ this.$('.o_mail_chat_add_more_search_bar').toggle();
+ this.$input.val('');
+ this.$input.focus();
},
- activate_user: function() {
- this.trigger("user_clicked", this.get("id"));
+ on_click_text: function(event){
+ event.preventDefault();
+ this._toggle_elements();
+ },
+ // to be redefined
+ do_search: function(search_val){
+ return $.when();
+ },
+ on_click_item: function(item){
+ if(item.value === '_create'){
+ if(this.last_search_val){
+ this.trigger('item_create', this.last_search_val);
+ }
+ }else{
+ this.trigger('item_clicked', item);
+ }
},
});
-var InstantMessaging = Widget.extend({
- template: "im_chat.InstantMessaging",
- events: {
- "keydown .oe_im_searchbox": "input_change",
- "keyup .oe_im_searchbox": "input_change",
- "change .oe_im_searchbox": "input_change",
- },
- init: function(parent) {
- this._super(parent);
- this.shown = false;
- this.set("right_offset", 0);
- this.set("current_search", "");
- this.users = [];
- this.widgets = {};
-
- // listen bus
- this.bus = bus.bus;
- this.bus.on("notification", this, this.on_notification);
+var PartnerAddMoreSeach = AbstractAddMoreSearch.extend({
+ /**
+ * Do the search call
+ * @override
+ */
+ do_search: function(search_val){
+ var self = this;
+ var Partner = new Model("res.partner");
+ return Partner.call('im_search', [search_val, this.limit]).then(function(result){
+ var values = [];
+ _.each(result, function(user){
+ values.push(_.extend(user, {
+ 'value': user.name,
+ 'label': user.name,
+ }));
+ });
+ return values;
+ });
},
- start: function() {
+});
+
+var ChannelAddMoreSearch = AbstractAddMoreSearch.extend({
+ /**
+ * Do the search call
+ * @override
+ */
+ do_search: function(search_val){
var self = this;
- // ui
- this.conv_manager = new im_chat_common.ConversationManager(this);
- this.on("change:right_offset", this.conv_manager, _.bind(function() {
- this.conv_manager.set("right_offset", this.get("right_offset"));
- }, this));
- $(window).scroll(_.bind(this.position_compute, this));
- $(window).resize(_.bind(this.position_compute, this));
- this.$el.css("right", -this.$el.outerWidth());
- this.position_compute();
-
- // business
- this.on("change:current_search", this, this.user_search);
-
- // add a drag & drop listener
- self.conv_manager.on("im_session_activated", self, function(conv) {
- conv.$el.droppable({
- drop: function(event, ui) {
- conv.add_user(ui.draggable.data("user"));
+ var Channel = new Model("mail.channel");
+ return Channel.call('channel_search_to_join', [search_val]).then(function(result){
+ var values = [];
+ _.each(result, function(channel){
+ values.push(_.extend(channel, {
+ 'value': channel.name,
+ 'label': channel.name,
+ }));
+ });
+ return values;
+ });
+ },
+});
+
+var PrivateGroupAddMoreSearch = AbstractAddMoreSearch.extend({
+ _bind_events: function(){
+ // don't call the super to avoid autocomplete
+ this.$input.on('keyup', this, this.on_keydown);
+ },
+ on_keydown: function(event){
+ if(event.which === $.ui.keyCode.ENTER && this.$input.val()){
+ this.trigger('item_create', this.$input.val());
+ }
+ },
+});
+
+
+/**
+ * Widget : Invite People to Channel Dialog
+ *
+ * Popup containing a 'many2many_tags' custom input to select multiple partners.
+ * Search user according to the input, and trigger event when selection is validated.
+ **/
+var PartnerInviteDialog = Dialog.extend({
+ dialog_title: _t('Invite people'),
+ template: "mail.chat.PartnerInviteDialog",
+ init: function(parent, options){
+ options = _.defaults(options || {}, {
+ buttons: [{
+ text: _t("Invite"),
+ close: true,
+ classes: "btn-primary",
+ click: _.bind(this.on_click_add, this),
+ }],
+ channel: undefined,
+ });
+ this._super.apply(this, arguments);
+ this.set("partners", []);
+ this.channel = options.channel;
+ this.PartnersModel = new Model('res.partner');
+ this.ChannelModel = new Model('mail.channel');
+ this.limit = 20;
+ },
+ start: function(){
+ var self = this;
+ this.$('.o_mail_chat_partner_invite_input').select2({
+ width: '100%',
+ allowClear: true,
+ multiple: true,
+ formatResult: function(item){
+ if(item.im_status === 'online'){
+ return '<span class="fa fa-circle"> ' + item.text + '</span>';
}
+ return '<span class="fa fa-circle-o"> ' + item.text + '</span>';
+ },
+ query: function (query) {
+ self.PartnersModel.call('im_search', [query.term, self.limit]).then(function(result){
+ var data = [];
+ _.each(result, function(partner){
+ partner['text'] = partner.name;
+ data.push(partner);
+ });
+ query.callback({results: data});
+ });
+ }
+ });
+ return this._super.apply(this, arguments);
+ },
+ on_click_add: function(){
+ var self = this;
+ var data = this.$('.o_mail_chat_partner_invite_input').select2('data');
+ if(data.length >= 1){
+ this.ChannelModel.call('channel_invite', [], {"ids" : [this.channel.id], 'partner_ids': _.pluck(data, 'id')}).then(function(){
+ var names = _.pluck(data, 'text').join(', ');
+ self.do_notify(_t('New people'), _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names));
+ self.close();
});
+ }else{
+ self.close();
+ }
+ },
+});
+
+/**
+ * Client Action : Instant Messaging View, inspired by Slack.com
+ *
+ * Action replacing the Inbox, and the list of group (mailing list, multiple conversation, rooms, ...)
+ * Includes real time messages (received and sent), creating group, channel, chat conversation, ...
+ **/
+var ChatMailThread = Widget.extend(mail_thread.MailThreadMixin, ControlPanelMixin, {
+ template: 'mail.chat.ChatMailThread',
+ events: {
+ // events from MailThreadMixin
+ "click .o_mail_redirect": "on_click_redirect",
+ "click .o_mail_thread_message_star": "on_message_star",
+ // events specific for ChatMailThread
+ "click .o_mail_chat_channel_item": "on_click_channel",
+ "click .o_mail_chat_partner_item": "on_click_partner",
+ "click .o_mail_chat_partner_unpin": "on_click_partner_unpin",
+ "click .o_mail_thread_message_needaction": "on_message_needaction",
+ },
+ init: function (parent, action) {
+ this._super.apply(this, arguments);
+ // attributes
+ this.action_manager = parent;
+ this.help_message = action.help || '';
+ this.context = action.context;
+ this.action = action;
+ this.loading_history = false;
+ this.chatter_needaction_auto = false;
+ this.options = this.action.params;
+ // mail thread mixin
+ mail_thread.MailThreadMixin.init.call(this);
+ // components : conversation manager and search widget (channel_type + '_search_widget')
+ this.conv_manager = web_client.mail_conversation_manager;
+ this.channel_search_widget = new ChannelAddMoreSearch(this, {'label': _t('+ Subscribe'), 'can_create': true});
+ this.group_search_widget = new PrivateGroupAddMoreSearch(this, {'label': _t('+ New private group'), 'can_create': true});
+ this.partner_search_widget = new PartnerAddMoreSeach(this);
+ // options (update the default of MailThreadMixin)
+ this.options = _.extend(this.options, {
+ 'display_document_link': true,
+ 'display_needaction_button': true,
+ 'emoji_list': this.conv_manager.emoji_list,
});
+ this.emoji_set_substitution(this.conv_manager.emoji_list);
+ // channel business
+ this.channels = {};
+ this.mapping = {}; // mapping partner_id/channel_id for 'direct message' channel
+ this.set('current_channel_id', false);
+ this.set('needaction_inbox_counter', 0);
+ this.search_domain = [];
+ // channel slots
+ this.set('channel_channel', []);
+ this.set('channel_direct_message', []);
+ this.set('channel_private_group', []);
+ this.set('partners', []);
+ // models
+ this.ChannelModel = new Model('mail.channel', this.context);
+ // control panel items
+ this.control_elements = {};
+ },
+ willStart: function(){
+ var self = this;
+ return session.rpc('/mail/client_action').then(function(result){
+ self.chatter_needaction_auto = result['chatter_needaction_auto'];
+ self.set('needaction_inbox_counter', result['needaction_inbox_counter']);
+ self.set('partners', result['channel_slots']['partners']);
+ self.mapping = result['channel_slots']['mapping'];
+ self._channel_slot(_.omit(result['channel_slots'], 'partners', 'mapping'));
+ });
+ },
+ start: function(){
+ var self = this;
+ this._super.apply(this, arguments);
+ mail_thread.MailThreadMixin.start.call(this);
+ // channel business events
+ this.on("change:current_channel_id", this, this.channel_change);
+ this.on("change:channel_channel", this, function(){
+ self.channel_render('channel_channel');
+ });
+ this.on("change:channel_private_group", this, function(){
+ self.channel_render('channel_private_group');
+ });
+ this.on("change:partners", this, this.partner_render);
- // fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page)
- return session.rpc("/im_chat/init", {}).then(function(notifications) {
- _.each(notifications, function(notif){
- self.conv_manager.on_notification(notif, {'load_history': true});
+ // search widget for channel
+ this.channel_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_channel'));
+ this.channel_search_widget.on('item_create', this, function(name){
+ self.channel_create(name, 'public');
+ });
+ this.channel_search_widget.on('item_clicked', this, function(item){
+ self.channel_join_and_get_info(item.id).then(function(channel){
+ self.channel_apply(channel);
+ });
+ });
+ // search widget for direct message
+ this.partner_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_partners'));
+ this.partner_search_widget.on('item_clicked', this, function(item){
+ self.channel_get([item.id]);
+ self.partner_add(item);
+ });
+ // search widget for private group
+ this.group_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_private_group'));
+ this.group_search_widget.on('item_create', this, function(name){
+ self.channel_create(name, 'private');
+ });
+
+ // bind event to load history when scrolling near top
+ this.$('.o_mail_chat_messages').scroll(function() {
+ if ($(this).scrollTop() <= 50 && self.loading_history) { // at 50px of the top, load history
+ self.message_load_history().then(function(messages){
+ if(messages.length < mail_thread.LIMIT_MESSAGE){
+ self.loading_history = false;
+ }
+ });
+ }
+ });
+
+ // needaction inbox counter
+ this.on('change:needaction_inbox_counter', this, this.needaction_inbox_change);
+
+ return $.when(this._super.apply(this, arguments), this.cp_render_searchview()).then(function(res){
+ // update control panel
+ self.cp_render_buttons();
+ // apply default channel
+ var channel_id = self.context.active_id || self.action.params['default_active_id'] || 'channel_inbox';
+ if(!_.isString(channel_id)){
+ if(_.contains(_.keys(self.channels), channel_id)){
+ self.set('current_channel_id', channel_id);
+ }else{
+ self.channel_info(channel_id).then(function(channel){
+ self.channel_apply(channel);
+ });
+ }
+ }else{
+ self.set('current_channel_id', channel_id);
+ }
+ // internal communication (bind here to avoid receving message from bus when client action still not totally ready)
+ internal_bus.on('mail_message_receive', self, self.message_receive);
+ internal_bus.on('mail_session_receive', self, self.channel_receive);
+ // needaction : the inbox has the same counter as the needaction top counter
+ internal_bus.on('mail_needaction_new', this, function(inc){
+ self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') + inc);
+ });
+ internal_bus.on('mail_needaction_done', this, function(dec){
+ self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') - dec);
});
- // start polling
- self.bus.start_polling();
});
},
- on_notification: function(notification){
- var channel = notification[0];
- var message = notification[1];
- // user status notification
- if(channel[1] === 'im_chat.presence'){
- if(message.im_status){
- this.user_update_status([message]);
+ // event actions
+ on_click_channel: function(event){
+ event.preventDefault();
+ var channel_id = this.$(event.currentTarget).data('channel-id');
+ this.set('current_channel_id', channel_id);
+ },
+ on_click_partner: function(event){
+ if(!this.$(event.target).hasClass('o_mail_chat_channel_unpin')){
+ event.preventDefault();
+ var partner_id = this.$(event.currentTarget).data('partner-id');
+ if(this.mapping[partner_id]){ // don't fetch if channel already in local
+ this.set('current_channel_id', this.mapping[partner_id]);
+ }else{
+ this.channel_get([partner_id]);
}
}
},
- // ui
- position_compute: function() {
- var top = 48;
- this.$el.css("top", top);
- this.$el.css("bottom", 0);
- },
- input_change: function() {
- this.set("current_search", this.$(".oe_im_searchbox").val());
- },
- switch_display: function() {
- this.position_compute();
- var fct = _.bind(function(place) {
- this.set("right_offset", place + this.$el.outerWidth());
- this.$(".oe_im_searchbox").focus();
- }, this);
- var opt = {
- step: fct,
- };
- if (this.shown) {
- this.$el.animate({
- right: -this.$el.outerWidth(),
- }, opt);
- } else {
- if (! bus.bus.activated) {
- this.do_warn("Instant Messaging is not activated on this server. Try later.", "");
- return;
+ on_click_partner_unpin: function(event){
+ event.preventDefault();
+ var self = this;
+ var $source = this.$(event.currentTarget);
+ var partner_id = $source.data('partner-id');
+ var channel_id = this.mapping[partner_id];
+ var channel = this.channels[channel_id];
+ this.channel_pin(channel.uuid, false).then(function(){
+ self.set('partners', _.filter(self.get('partners'), function(p){ return p.id !== partner_id; }));
+ self.channel_remove(channel_id);
+ delete self.mapping[partner_id];
+ // if unpin current channel, switch to inbox
+ if(self.get('current_channel_id') === channel_id){
+ self.set('current_channel_id', 'channel_inbox');
}
- // update the list of user status when show the IM
- this.user_search();
- this.$el.animate({
- right: 0,
- }, opt);
+ });
+ },
+ on_click_button_minimize: function(event){
+ event.preventDefault();
+ var current_channel = this.channels[this.get('current_channel_id')];
+ if(current_channel){
+ var channel_uuid = current_channel.uuid;
+ return this.ChannelModel.call("channel_minimize", [channel_uuid, true]);
}
- this.shown = ! this.shown;
+ return $.Deferred.reject();
},
- // user methods
- user_search: function(e) {
+ on_click_button_invite: function(event){
+ var dialog = new PartnerInviteDialog(this, {
+ title: _.str.sprintf(_t('Invite people to %s.'), this.get_current_channel_name()),
+ channel: this.channels[this.get('current_channel_id')],
+ }).open();
+ },
+ on_click_button_unsubscribe: function(event){
var self = this;
- var user_model = new Model("res.users");
- return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){
- self.$(".oe_im_input").val("");
- var old_widgets = self.widgets;
- self.widgets = {};
- self.users = [];
- _.each(result, function(user) {
- user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id});
- var widget = new UserWidget(self, user);
- widget.appendTo(self.$(".oe_im_users"));
- widget.on("user_clicked", self, self.user_clicked);
- self.widgets[user.id] = widget;
- self.users.push(user);
- });
- _.each(old_widgets, function(w) {
- w.destroy();
- });
+ this.ChannelModel.call('action_unfollow', [[this.get('current_channel_id')]]).then(function(){
+ var channel = self.channels[self.get('current_channel_id')]
+ var slot = self.get_channel_slot(channel);
+ self.set(slot, _.filter(self.get(slot), function(c){ return c.id !== channel.id; }));
+ self.do_notify(_t('Unsubscribe'), _.str.sprintf(_t('You unsubscribe from <b>%s</b>.'), self.get_current_channel_name()));
+ self.set('current_channel_id', 'channel_inbox'); // jump to inbox
});
},
- user_clicked: function(user_id) {
+ on_click_button_settings: function(event){
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: "mail.channel",
+ res_id: this.get('current_channel_id'),
+ views: [[false, 'form']],
+ target: 'current'
+ }, {
+ clear_breadcrumbs: true
+ });
+ },
+ on_message_needaction: function(event){
var self = this;
- var sessions = new Model("im_chat.session");
- return sessions.call("session_get", [user_id]).then(function(session) {
- self.conv_manager.session_apply(session, {'focus': true, 'force_open': true});
+ mail_thread.MailThreadMixin.on_message_needaction.call(this, event).then(function(message_id){
+ // decrement the needaction top counter
+ internal_bus.trigger('mail_needaction_done', 1);
+ // decrement the channel labels
+ var treated_messages = _.filter(self.get('messages'), function(m){ return m.id == message_id; });
+ if(treated_messages.length){
+ self.needaction_decrement(treated_messages[0].channel_ids || []);
+ }
});
},
- user_update_status: function(users_list){
+ // control panel
+ cp_update: function(){
var self = this;
- _.each(users_list, function(el) {
- if (self.widgets[el.id]) {
- self.widgets[el.id].set("im_status", el.im_status);
+ var current_channel_id = this.get('current_channel_id');
+ var current_channel_name = this.get_current_channel_name();
+ // toggle cp buttons
+ if(_.isString(current_channel_id)){
+ self.control_elements.$buttons.find('button').hide();
+ }else{
+ self.control_elements.$buttons.find('button').show();
+ var current_channel = this.channels[current_channel_id];
+ if(current_channel.channel_type == 'chat'){
+ self.control_elements.$buttons.find('.o_mail_chat_button_invite, .o_mail_chat_button_more').hide();
}
+ }
+ // update control panel
+ var status = {
+ breadcrumbs: this.action_manager.get_breadcrumbs().concat([{'title': current_channel_name, 'action': _.clone(this)}]),
+ cp_content: {
+ $buttons: self.control_elements.$buttons,
+ $searchview: self.control_elements.$searchview,
+ $searchview_buttons: self.control_elements.$searchview_buttons,
+ $switch_buttons: false,
+ $pager: false,
+ },
+ searchview: self.searchview,
+ };
+ this.update_control_panel(status, {clear: false});
+ },
+ cp_render_buttons: function() {
+ var self = this;
+ this.control_elements.$buttons = $(QWeb.render("mail.chat.ControlButtons", {}));
+ this.control_elements.$buttons.on('click', '.o_mail_chat_button_minimize', _.bind(this.on_click_button_minimize, this));
+ this.control_elements.$buttons.on('click', '.o_mail_chat_button_invite', _.bind(this.on_click_button_invite, this));
+ this.control_elements.$buttons.on('click', '.o_mail_chat_button_unsubscribe', _.bind(this.on_click_button_unsubscribe, this));
+ this.control_elements.$buttons.on('click', '.o_mail_chat_button_settings', _.bind(this.on_click_button_settings, this));
+ },
+ cp_render_searchview: function(){
+ var self = this;
+ var options = {
+ $buttons: $("<div>"),
+ action: this.action,
+ };
+ var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false;
+ this.searchview = new SearchView(this, this.MessageDatasetSearch, view_id, {}, options);
+
+ this.searchview.on('search_data', this, this.on_search);
+ return $.when(this.searchview.appendTo($("<div>"))).done(function() {
+ self.control_elements.$searchview = self.searchview.$el;
+ self.control_elements.$searchview_buttons = self.searchview.$buttons.contents();
+ // hack to hide 'group by' button, since it is not relevant
+ self.searchview.$buttons.find('.oe-groupby-menu').hide();
+ });
+ },
+ /**
+ * Method call when action if done after redirecting (using 'on_click_redirect')
+ * @override
+ */
+ on_reverse_breadcrumb: function(){
+ var current_channel_id = this.get('current_channel_id');
+ this.cp_update(); // do not reload the client action, just display it, but a refresh of the control panel is needed.
+ // push state
+ this.action_manager.do_push_state({
+ action: this.action.id,
+ active_id: current_channel_id,
+ });
+ },
+ // messages search methods
+ on_search: function(domains, contexts, groupbys){
+ var self = this;
+ return pyeval.eval_domains_and_contexts({
+ domains: [[]].concat(domains || []),
+ contexts: [this.context].concat(contexts || []),
+ group_by_seq: groupbys || []
+ }).done(function(results){
+ if (results.error) {
+ throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error)));
+ }
+ // modify the search domain and do search
+ self.search_domain = results.domain;
+ return self.message_load_new();
+ });
+ },
+ // channels
+ /**
+ * Set the channel slot
+ * @param {Object[]} fetch_result : should contains only the slot name (as key) and the list of channel header as value.
+ */
+ _channel_slot: function(fetch_result){
+ var self = this;
+ var channel_slots = _.keys(fetch_result);
+ _.each(channel_slots, function(slot){
+ // update the channel slot
+ self.set(slot, fetch_result[slot]);
+ // flatten the result : update the complete channel list
+ _.each(fetch_result[slot], function(channel){
+ self.channels[channel.id] = channel;
+ });
+ });
+ },
+ /**
+ * Apply a channel means adding it, and swith to it
+ * @param {Object} channel : channel header
+ */
+ channel_apply: function(channel){
+ this.channel_add(channel);
+ this.set('current_channel_id', channel.id);
+ },
+ /**
+ * Add the given channel, or update it if already exists and loaded
+ * @param {Object} channel : channel header to add
+ */
+ channel_add: function(channel){
+ var channel_slot = this.get_channel_slot(channel);
+ var existing = this.get(channel_slot);
+ if(_.contains(_.pluck(existing, 'id'), channel.id)){
+ // update the old channel
+ var filtered_channels = _.filter(existing, function(item){ return item.id != channel.id; });
+ this.set(channel_slot, filtered_channels.concat([channel]));
+ }else{
+ // simply add the reveiced channel
+ this.set(channel_slot, existing.concat([channel]));
+ }
+ // also update the flatten list
+ this.channels[channel.id] = channel;
+
+ // update the mapping for 'direct message' channel, and the partner list
+ if(channel_slot === 'channel_direct_message'){
+ var partner = channel.direct_partner[0];
+ this.mapping[partner.id] = channel.id;
+ this.partner_add(partner);
+ }
+ },
+ channel_remove: function(channel_id){
+ var channel = this.channels[channel_id];
+ var slot = this.get_channel_slot(channel);
+ this.set(slot, _.filter(this.get(slot), function(c){ return c.id !== channel_id; }));
+ delete this.channels[channel_id];
+ },
+ /**
+ * Get the channel the current user has with the given partner, and get the channel header
+ * @param {Number[]} partner_ids : list of res.partner identifier
+ */
+ channel_get: function(partner_ids){
+ var self = this;
+ return this.ChannelModel.call('channel_get', [partner_ids]).then(function(channel){
+ self.channel_apply(channel);
+ });
+ },
+ /**
+ * Create a channel with the given name and type, and apply it
+ * @param {String} channel_name : the name of the channel
+ * @param {String} privacy : the privacy of the channel (groups, public, ...)
+ */
+ channel_create: function(channel_name, privacy){
+ var self = this;
+ return this.ChannelModel.call('channel_create', [channel_name, privacy]).then(function(channel){
+ self.channel_apply(channel);
+ });
+ },
+ channel_info: function(channel_id){
+ var self = this;
+ return this.ChannelModel.call('channel_info', [[channel_id]]).then(function(channels){
+ return channels[0];
+ });
+ },
+ channel_pin: function(uuid, pinned){
+ return this.ChannelModel.call('channel_pin', [uuid, pinned]);
+ },
+ channel_join_and_get_info: function(channel_id){
+ return this.ChannelModel.call('channel_join_and_get_info', [[channel_id]]);
+ },
+ channel_change: function(){
+ var self = this;
+ var current_channel_id = this.get('current_channel_id');
+ var current_channel = this.channels[current_channel_id];
+ // mail chat compose message (destroy and replace it)
+ this.mail_chat_compose_message && this.mail_chat_compose_message.destroy();
+ this.mail_chat_compose_message = new mail_thread.MailComposeMessage(this, new data.DataSetSearch(this, 'mail.channel', this.context), {
+ 'emoji_list': this.options.emoji_list,
+ 'context': _.extend(this.context, {
+ 'default_res_id': current_channel_id,
+ }),
+ 'display_mode': 'chat',
+ });
+ if(_.isString(current_channel_id)){
+ this.$('.o_mail_chat_composer_wrapper').hide();
+ }else{
+ this.$('.o_mail_chat_composer_wrapper').show();
+ }
+ this.mail_chat_compose_message.appendTo(this.$('.o_mail_chat_composer_wrapper'));
+ this.mail_chat_compose_message.focus();
+
+ // push state (the action is referred by action_manager, and no reloaded when jumping
+ // channel, so update context is requried)
+ this.action.context['active_id'] = current_channel_id;
+ this.action.context['active_ids'] = [current_channel_id];
+ web_client.action_manager.do_push_state({
+ action: this.action.id,
+ active_id: current_channel_id,
});
- }
-});
+ // update control panel
+ this.cp_update();
-var ImTopButton = Widget.extend({
- template:'im_chat.ImTopButton',
- events: {
- "click": "clicked",
+ // fetch the messages
+ return this.message_load_new().then(function(raw_messages){
+ // allow loading history
+ self.loading_history = true;
+ // if normal channel, update last message id, and unbold
+ var def = $.Deferred().resolve();
+ if(_.isNumber(current_channel_id)){
+ def = self.ChannelModel.call('channel_seen', [[current_channel_id]]);
+ }else{
+ // auto treat needaction from inbox
+ if(current_channel_id === 'channel_inbox' && self.chatter_needaction_auto){
+ def = self.MessageDatasetSearch._model.query(['id', 'channel_ids']).filter([['needaction', '=', true]]).all().then(function(messages){
+ // get needaction message ids
+ var message_ids = _.pluck(messages, 'id');
+ if(message_ids){
+ // call to unlink
+ self.MessageDatasetSearch._model.call('set_message_done',[message_ids]).then(function(){
+ // decrement channel items in batch
+ var flatten_channel_ids = _.flatten(_.pluck(messages, 'channel_ids'));
+ var channel_count = _.countBy(flatten_channel_ids, function(cid) {
+ return cid;
+ });
+ _.each(_.keys(channel_count), function(cid){
+ self.needaction_decrement([cid], channel_count[cid]);
+ });
+ // decrement the needaction top counter
+ internal_bus.trigger('mail_needaction_done', message_ids.length);
+ });
+ }
+ });
+ }
+ }
+ def.then(function(){
+ self._toggle_unread_message(current_channel_id, false, true);
+ self._scroll();
+ });
+ });
},
- start: function() {
- // Create the InstantMessaging widget and put it in the DOM
- var im = new InstantMessaging(this);
- web_client.im_messaging = im;
- im.appendTo(web_client.$el);
- // Bind the click action to the ImTopButton
- this.on("clicked", im, im.switch_display);
- return this._super();
+ channel_render: function(channel_slot){
+ this.$('.o_mail_chat_channel_slot_' + channel_slot).replaceWith(QWeb.render("mail.chat.ChatMailThread.channels", {'widget': this, 'channel_slot': channel_slot}));
+ },
+ // partners
+ partner_add: function(partner){
+ var partners = _.filter(this.get('partners'), function(p){ return p.id != partner.id; });
+ this.set('partners', partners.concat([partner]));
+ },
+ partner_render: function(){
+ this.$('.o_mail_chat_channel_slot_partners').replaceWith(QWeb.render("mail.chat.ChatMailThread.partners", {'widget': this}));
+ },
+ // needaction
+ needaction_increment: function(channel_ids, inc){
+ var self = this;
+ _.each(channel_ids, function(channel_id){
+ if(self.channels[channel_id]){
+ var count = (self.channels[channel_id].message_needaction_counter || 0) + (inc || 1);
+ self.channels[channel_id].message_needaction_counter = count;
+ self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count);
+ self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show();
+ }
+ });
},
- clicked: function(ev) {
- ev.preventDefault();
- this.trigger("clicked");
+ needaction_decrement: function(channel_ids, dec){
+ var self = this;
+ _.each(channel_ids, function(channel_id){
+ if(self.channels[channel_id]){
+ var count = (self.channels[channel_id].message_needaction_counter || 0) - (dec || 1);
+ self.channels[channel_id].message_needaction_counter = count;
+ self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count);
+ if(count <= 0){
+ self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show();
+ }
+ }
+ });
+ },
+ needaction_inbox_change: function(inc){
+ var count = this.get('needaction_inbox_counter');
+ var $elem = this.$('.o_mail_chat_needaction[data-channel-id="channel_inbox"]');
+ $elem.html(count);
+ if(count > 0){
+ $elem.show();
+ }else{
+ $elem.hide();
+ }
+ },
+ // from bus
+ channel_receive: function(channel){
+ this.channel_add(channel);
+ },
+ message_receive: function(message){
+ var self = this;
+ // if current channel should reveice message, give it to it
+ if(_.contains(message['channel_ids'], this.get('current_channel_id'))){
+ this.message_insert([message]);
+ }
+ // for other message channel, get the channel if not loaded yet, and bolded them
+ var other_message_channel_ids = _.without(message['channel_ids'], this.get('current_channel_id'));
+ var active_channel_ids = _.map(_.keys(this.channels), function(cid){
+ return parseInt(cid);
+ }); // integer as key of a dict is cast as string in javascript
+ var channel_to_fetch = _.difference(other_message_channel_ids, active_channel_ids);
+ // fetch unloaded channels and add it
+ var def = $.Deferred();
+ if(channel_to_fetch.length >= 1){
+ def = this.ChannelModel.call("channel_info", [], {"ids" : channel_to_fetch}).then(function(channels){
+ _.each(channels, function(channel){
+ self.channel_add(channel);
+ });
+ });
+ }else{
+ def.resolve([]);
+ }
+ // bold the channel to indicate unread messages
+ def.then(function(){
+ // bold channel having unread messages
+ _.each(other_message_channel_ids, function(channel_id){
+ self._toggle_unread_message(channel_id, true);
+ });
+ // auto scroll to bottom
+ self._scroll();
+ });
+ // if needaction, then increment the label
+ if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){
+ this.needaction_increment(message.channel_ids || []);
+ }
+ },
+ // utils function
+ get_channel_slot: function(channel){
+ if(channel.channel_type === 'channel'){
+ if(channel.public === 'private'){
+ return 'channel_private_group';
+ }
+ return 'channel_channel';
+ }
+ if(channel.channel_type === 'chat'){
+ return 'channel_direct_message';
+ }
+ },
+ get_current_channel_name: function(){
+ var current_channel_id = this.get('current_channel_id');
+ var current_channel = this.channels[current_channel_id];
+ var current_channel_name = current_channel && current_channel.name || _t('Unknown');
+ // virtual channel id (for inbox, or starred channel)
+ if(_.isString(current_channel_id)){
+ if(current_channel_id == 'channel_inbox'){
+ current_channel_name = _t('Inbox');
+ }
+ if(current_channel_id == 'channel_starred'){
+ current_channel_name = _t('Starred');
+ }
+ }
+ return current_channel_name;
+ },
+ _toggle_unread_message: function(channel_id, add_or_remove, set_as_active){
+ var partner_id = false;
+ var inverse_mapping = _.invert(this.mapping);
+ // toggle bold channel/partner item
+ this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove);
+ if(_.contains(_.keys(inverse_mapping), channel_id.toString())){
+ partner_id = parseInt(inverse_mapping[channel_id]);
+ this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove);
+ }
+ // set the channel/partner item as the active one
+ if(set_as_active){
+ this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item, .o_mail_chat_sidebar .o_mail_chat_partner_item').find('a').removeClass('active');
+ this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').find('a').addClass('active');
+ if(partner_id){
+ this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').find('a').addClass('active');
+ }
+ }
+ },
+ _scroll: function(){
+ var current_channel_id = this.get('current_channel_id');
+ var current_channel = this.channels[current_channel_id];
+ if(_.isNumber(current_channel_id)){
+ if(current_channel.seen_message_id){
+ if(this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]")){
+ this.$('.o_mail_chat_messages').scrollTop(this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]").offset().top);
+ }else{
+ this.$('.o_mail_chat_messages').scrollTop(0);
+ }
+ }else{
+ this.$('.o_mail_chat_messages').scrollTop(this.$('.o_mail_chat_messages')[0].scrollHeight);
+ }
+ current_channel.seen_message_id = false;
+ }else{
+ this.$('.o_mail_chat_messages').scrollTop(this.$('.o_mail_chat_messages')[0].scrollHeight);
+ }
+ },
+ /**
+ * Render the messages
+ * @override
+ */
+ message_render: function(){
+ this.$('.o_mail_chat_messages_content').html(QWeb.render('mail.chat.ChatMailThread.content', {'widget': this}));
+ },
+ /**
+ * Return the message domain for the current channel
+ * @override
+ */
+ get_message_domain: function(){
+ // default channel domain
+ var current_channel_id = this.get('current_channel_id');
+ var domain = [['channel_ids', 'in', current_channel_id]];
+ // virtual channel id (for inbox, or starred channel)
+ if(_.isString(current_channel_id)){
+ if(current_channel_id == 'channel_inbox'){
+ domain = [['needaction', '=', true]];
+ }
+ if(current_channel_id == 'channel_starred'){
+ domain = [['starred', '=', true]];
+ }
+ }
+ // add search domain
+ domain = domain.concat(this.search_domain);
+ return domain;
},
});
-// Put the ImTopButton widget in the systray menu if the user is an employee
-var Users = new Model('res.users');
-Users.call('has_group', ['base.group_user']).done(function(is_employee) {
- if (is_employee) {
- SystrayMenu.Items.push(ImTopButton);
- }
-});
+core.action_registry.add('mail.chat.instant_messaging', ChatMailThread);
+
return {
- InstantMessaging: InstantMessaging,
- UserWidget: UserWidget,
+ ChatMailThread: ChatMailThread,
};
});
diff --git a/addons/mail/static/src/js/mail_chat_common.js b/addons/mail/static/src/js/mail_chat_common.js
index 0aaa0e68d16..a5f52ddf4b1 100644
--- a/addons/mail/static/src/js/mail_chat_common.js
+++ b/addons/mail/static/src/js/mail_chat_common.js
@@ -1,13 +1,12 @@
-odoo.define('im_chat.im_chat_common', function (require) {
+odoo.define('mail.chat_common', function (require) {
"use strict";
-// to do: make this work in website
-
var bus = require('bus.bus');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');
var Widget = require('web.Widget');
+var mail_utils = require('mail.utils');
var _t = core._t;
var QWeb = core.qweb;
@@ -15,140 +14,130 @@ var QWeb = core.qweb;
var NBR_LIMIT_HISTORY = 20;
+/**
+ * Widget handeling the sessions/conversations
+ *
+ * Responsible to listen the bus and do the correct action according to the received notifications (fetch
+ * conversation, add message, ...)
+ **/
var ConversationManager = Widget.extend({
init: function(parent, options) {
var self = this;
this._super(parent);
this.options = _.clone(options) || {};
_.defaults(this.options, {
- inputPlaceholder: _t("Say something..."),
- defaultMessage: null,
- defaultUsername: _t("Visitor"),
- force_open: false,
+ placeholder: _t("Say something..."),
+ default_username: _t("Visitor"),
load_history: true,
focus: false,
});
+ this.emoji_list = [];
// business
this.sessions = {};
this.bus = bus.bus;
this.bus.on("notification", this, this.on_notification);
- this.bus.options.im_presence = true;
-
// ui
this.set("right_offset", 0);
this.set("bottom_offset", 0);
this.on("change:right_offset", this, this.calc_positions);
this.on("change:bottom_offset", this, this.calc_positions);
- this.set("window_focus", true);
- this.on("change:window_focus", self, function(e) {
- self.bus.options.im_presence = self.get("window_focus");
- });
this.set("waiting_messages", 0);
this.on("change:waiting_messages", this, this.window_title_change);
$(window).on("focus", _.bind(this.window_focus, this));
- $(window).on("blur", _.bind(this.window_blur, this));
this.window_title_change();
+ // session widgets deferred (for their creation)
+ this._session_defs = {};
+ },
+ start: function(){
+ var self = this;
+ return $.when(session.rpc("/mail/chat_init"), this._super.apply(this, arguments)).then(function(result){
+ self._setup(result);
+ });
+ },
+ _setup: function(init_data){
+ this.emoji_list = init_data['emoji'];
+ this.options['emoji'] = mail_utils.shortcode_substitution(init_data['emoji']);
},
on_notification: function(notification, options) {
var self = this;
var channel = notification[0];
var message = notification[1];
var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
+ var is_channel_uuid = regex_uuid.test(channel);
- // Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session)
- if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){
- // message to display in the chatview
- if (message.type === "message" || message.type === "meta") {
- self.message_receive(message);
- }
+ // (dbname, 'res.partner', partner_id) receive only the new mail.channel header
+ if((Array.isArray(channel) && (channel[1] === 'res.partner')) || is_channel_uuid){
// activate the received session
if(message.uuid){
- this.session_apply(message, options);
+ this.session_apply(message, options);
+ }
+ }
+ // (dbname, 'mail.channel', channel_id) receive only the mail.message of the channel
+ if((Array.isArray(channel) && (channel[1] === 'mail.channel')) || is_channel_uuid){
+ // message to display in the chatview
+ if (message.message_type) {
+ self.message_receive(message);
}
}
},
-
// window focus unfocus beep and title
window_focus: function() {
- this.set("window_focus", true);
this.set("waiting_messages", 0);
},
- window_blur: function() {
- this.set("window_focus", false);
- },
window_beep: function() {
- if (typeof(Audio) === "undefined") {
- return;
- }
- var audio = new Audio();
- var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
- audio.src = session.url("/im_chat/static/src/audio/ting") + ext;
- audio.play();
+ mail_utils.beep(session);
},
window_title_change: function() {
var title = undefined;
if (this.get("waiting_messages") !== 0) {
- title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"));
this.window_beep();
}
},
- // sessions and messages
- session_apply: function(active_session, options){
- // options used by this function : force_open and focus (load_history can be usefull)
+ // sessions methods
+ session_apply: function(session, options){
+ // options used by this function : focus (load_history can be usefull)
var self = this;
- options = _.extend(_.clone(this.options), options || {});
- // force open
- var session = _.clone(active_session);
- if(options['force_open']){
- session.state = 'open';
- }
+ options = _.defaults(options || {}, {
+ 'focus': true,
+ }, this.options);
// create/get the conversation widget
- var conv = this.sessions[session.uuid];
+ var conv = this.sessions[session.id];
if (! conv) {
if(session.state !== 'closed'){
- conv = new Conversation(this, this, session, options);
- conv.appendTo($("body"));
+ conv = new Conversation(this, session, options);
+ // store this deferred to apply next action (notifications) when it is resolved.
+ this._session_defs[session.id] = conv.appendTo($("body"));
+ this._session_defs[session.id].then(function() {
+ self.calc_positions();
+ });
conv.on("destroyed", this, _.bind(this.session_delete, this));
- this.sessions[session.uuid] = conv;
- this.calc_positions();
+ this.sessions[session.id] = conv;
}
}else{
- conv.set("session", session);
- }
- // if force open, broadcast it
- if(options['force_open'] && active_session.state !== 'open'){
- conv.session_update_state('open');
- }
- // apply the focus
- if (options['focus']){
- conv.focus();
+ // update the state only when the 'contruction' deferred is resolved.
+ this._session_defs[session.id].then(function(){
+ conv.set('session', session);
+ });
}
return conv;
},
- session_delete: function(uuid){
- delete this.sessions[uuid];
+ session_delete: function(channel_id){
+ delete this.sessions[channel_id];
this.calc_positions();
},
- message_receive: function(message) {
+ /**
+ * Adding a message in all its channels
+ * /!\ Suppose the channel are already loaded in the ConversationManager /!\
+ * @param {Object} message : the received message (formatted server side)
+ */
+ message_receive: function(message){
var self = this;
- var session_id = message.to_id[0];
- var uuid = message.to_id[1];
- var from_id = message['from_id'] ? message['from_id'][0] : false;
- if (! this.get("window_focus") && from_id != this.get_current_uid()) {
- this.set("waiting_messages", this.get("waiting_messages") + 1);
- }
- this._message_receive(message);
- },
- _message_receive: function(message){
- var uuid = message.to_id[1];
- var conv = this.sessions[uuid];
- conv.message_receive(message);
+ _.each(message.channel_ids, function(channel_id){
+ self.sessions[channel_id].message_receive(message);
+ });
},
// others
- get_current_uid: function(){
- return session && !_.isUndefined(session.uid) ? session.uid : false;
- },
calc_positions: function() {
var self = this;
var current = this.get("right_offset");
@@ -159,84 +148,72 @@ var ConversationManager = Widget.extend({
});
},
destroy: function() {
- $(window).off("unload", this.unload);
$(window).off("focus", this.window_focus);
$(window).off("blur", this.window_blur);
return this._super();
}
});
-
+/**
+ * Widget Conversation
+ *
+ * Display the conversation in a 'portable' window, manage sending and receiving message,
+ * and the fold state of the conversation.
+ **/
var Conversation = Widget.extend({
- className: "openerp_style oe_im_chatview",
+ template: "mail.chat.im.Conversation",
events: {
"keydown input": "keydown",
- "click .oe_im_chatview_close": "click_close",
- "click .oe_im_chatview_header": "click_header"
+ "click .o_mail_chat_im_window_close": "on_click_close",
+ "click .o_mail_chat_im_window_header": "on_click_header"
},
- init: function(parent, c_manager, session, options) {
+ init: function(parent, session, options) {
this._super(parent);
- this.c_manager = c_manager;
- this.options = options || {};
- this.loading_history = true;
+ this.options = _.defaults(options || {}, {
+ 'emoji': {},
+ 'placeholder': _t('Write something ...'),
+ 'loading_history': true,
+ 'focus': true,
+ });
this.set("messages", []);
this.set("session", session);
+ this.set("pending", 0);
this.set("right_position", 0);
this.set("bottom_position", 0);
- this.set("pending", 0);
- this.inputPlaceholder = this.options.defaultInputPlaceholder;
+ },
+ willStart: function(){
+ if(this.options.loading_history){
+ return $.when(this.message_history(), this._super.apply(this, arguments));
+ }
+ return this._super.apply(this, arguments);
},
start: function() {
var self = this;
- self._super.apply(this, arguments);
- self.prepare_action_menu();
// ui
- self.$().append(QWeb.render("im_chat.Conversation", {widget: self}));
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
- self.on("change:pending", self, _.bind(function() {
- if (self.get("pending") === 0) {
- self.$(".nbr_messages").text("");
- } else {
- self.$(".nbr_messages").text("(" + self.get("pending") + ")");
- }
- }, self));
+ self.on("change:pending", self, self.message_pending_change);
+
self.full_height = self.$().height();
- self.calc_pos();
- // business
- self.bind_action_menu();
- self.on("change:session", self, self.session_update);
- self.on("change:messages", self, self.render_messages);
- self.$('.oe_im_chatview_content').on('scroll',function(){
+ self.$('.o_mail_chat_im_window_content').on('scroll',function(){
if($(this).scrollTop() === 0){
self.message_history();
}
});
- if(self.options['load_history']){ // load history if asked
- self.message_history();
- }
- // prepare the header and the correct state
- self.session_update();
- },
- // action menu
- _add_action: function(label, style_class, icon_fa_class, callback){
- this.actions.push({
- 'label': label,
- 'class': style_class,
- 'icon_class': icon_fa_class,
- 'callback': callback,
- });
- },
- prepare_action_menu: function(){
- // override this method to add action with _add_action()
- this.actions = [];
- },
- bind_action_menu: function(){
- var self = this;
- _.each(this.actions, function(action){
- if(action.callback){
- self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self));
+ // business
+ self.on("change:session", self, self.session_change);
+ self.on("change:messages", self, self.message_render);
+
+ return self._super.apply(this, arguments).then(function(){
+ // place the conversation window correctly on the screen
+ self.calc_pos();
+ // focus if needed
+ if(self.options.focus){
+ self.focus();
}
+ // re-apply the session to display the correct state
+ self.set('session', _.clone(self.get('session')));
+ self._go_bottom();
});
},
// ui
@@ -248,7 +225,7 @@ var Conversation = Widget.extend({
},
hide: function(){
this.$().animate({
- height: this.$(".oe_im_chatview_header").outerHeight()
+ height: this.$(".o_mail_chat_im_window_header").outerHeight()
});
},
calc_pos: function() {
@@ -256,21 +233,25 @@ var Conversation = Widget.extend({
this.$().css("bottom", this.get("bottom_position"));
},
// session
- session_update_state: function(state){
+ /**
+ * Set the fold state LOCALLY for the session. This will
+ * trigger a ui update
+ * @param {string} state : the fold state ('fold', 'closed' or 'open')
+ */
+ session_fold: function(state){
var session = this.get('session');
session.state = state;
this.set('session', session);
},
- session_update: function(){
- // built the name
- var names = [];
- _.each(this.get("session").users, function(user){
- if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){
- names.push(user.name);
- }
- });
- this.$(".header_name").text(names.join(", "));
- this.$(".header_name").attr('title', names.join(", "));
+ /**
+ * Triggered when the session has changed : it update the header name
+ * and fold the window (ui) according to its state.
+ */
+ session_change: function(){
+ // update conversation name in header
+ var name = this.get("session").name || _t('Unknown Channel');
+ this.$(".o_mail_chat_im_window_header_name").text(name);
+ this.$(".o_mail_chat_im_window_header_name").attr('title', name);
// update the fold state
if(this.get("session").state){
if(this.get("session").state === 'closed'){
@@ -285,39 +266,53 @@ var Conversation = Widget.extend({
}
},
// messages
+ /**
+ * Load the history of the current session, and insert it into already loaded messages
+ * @returns {Deferred} resolved with the new loaded messages (Object[])
+ */
message_history: function(){
var self = this;
- if(this.loading_history){
- var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY};
- var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false;
- if(lastid){
- data.last_id = lastid;
- }
- session.rpc("/im_chat/history", data).then(function(messages){
+ if(this.options.loading_history){
+ var data = {
+ uuid: self.get("session").uuid,
+ limit: NBR_LIMIT_HISTORY,
+ last_id: _.first(this.get("messages")) ? _.first(this.get("messages")).id : false,
+ };
+ return session.rpc("/mail/chat_history", data).then(function(messages){
if(messages){
- self.insert_messages(messages);
+ self.message_insert(messages);
if(messages.length != NBR_LIMIT_HISTORY){
- self.loading_history = false;
+ self.options.loading_history = false;
}
}else{
- self.loading_history = false;
+ self.options.loading_history = false;
}
+ return messages;
});
}
+ return $.Deferred().resolve([]);
},
message_receive: function(message) {
- if (this.get('session').state === 'open') {
- this.set("pending", 0);
- } else {
- this.set("pending", this.get("pending") + 1);
+ // don't add duplicated message (this can happen when refreshing and receiving already recieved
+ // message that are still in the bus queue/buffer).
+ if(!_.contains(_.pluck(this.get('messages'), 'id'), message.id)){
+ // is pending message ?
+ if (this.get('session').state === 'open') {
+ this.set("pending", 0);
+ } else {
+ if(!this.message_am_i_author(message)){
+ this.set("pending", this.get("pending") + 1);
+ }
+ }
+ // insert it
+ this.message_insert([message]);
+ this._go_bottom();
}
- this.insert_messages([message]);
- this._go_bottom();
},
- message_send: function(message, type) {
+ message_send: function(message) {
var self = this;
var send_it = function() {
- return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
+ return session.rpc("/mail/chat_post", {uuid: self.get("session").uuid, message_content: message});
};
var tries = 0;
send_it().fail(function(error, e) {
@@ -327,71 +322,65 @@ var Conversation = Widget.extend({
return send_it();
});
},
- insert_messages: function(messages){
+ /**
+ * Preporcess the message (add informations in the Object, ...)
+ * @param {Object[]} messages : server side formatted message (from message_format method)
+ */
+ _message_preprocess: function(raw_messages){
var self = this;
- // avoid duplicated messages
- messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; });
- // escape the message content and set the timezone
- _.map(messages, function(m){
- if(!m.from_id){
- m.from_id = [false,self. get_anonymous_name()];
+ _.each(raw_messages, function(m){
+ m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone
+ if(m.body){
+ m.body = mail_utils.shortcode_apply(m.body, self.options.emoji);
}
- m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss');
- return m;
+ _.each(m.attachment_ids, function(a){
+ a.url = mail_utils.get_attachment_url(session, m.id, a.id);
+ a.fa_class = mail_utils.attachment_filetype_to_fa_class(a.file_type_icon);
+ });
});
+ return raw_messages;
+ },
+ /**
+ * Insert new raw message into the current ones
+ * @param {Object[]} messages : server side formatted message (from message_format method)
+ */
+ message_insert: function(raw_messages){
+ var messages = this._message_preprocess(raw_messages);
this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
},
- render_messages: function(){
+ message_render: function(){
var self = this;
- var res = {};
- var last_date_day, last_user_id = -1;
- _.each(this.get("messages"), function(current){
- // add the url of the avatar for all users in the conversation
- current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0]));
- var date_day = current.create_date.split(" ")[0];
- if(date_day !== last_date_day){
- res[date_day] = [];
- last_user_id = -1;
- }
- last_date_day = date_day;
- if(current.type == "message"){ // traditionnal message
- if(last_user_id === current.from_id[0]){
- _.last(res[date_day]).push(current);
- }else{
- res[date_day].push([current]);
- }
- last_user_id = current.from_id[0];
- }else{ // meta message
- res[date_day].push([current]);
- last_user_id = -1;
- }
- });
- // render and set the content of the chatview
- // TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template
- // since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be
- // imported in the frontend.
- this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res})));
- this._go_bottom();
+ var message_html = $(QWeb.render("mail.chat.im.Conversation.messages", {"widget": this}));
+ this.$('.o_mail_chat_im_window_content').html(message_html);
+ },
+ message_am_i_author: function(message){
+ return message.author_id && message.author_id[0] === session.partner_id;
+ },
+ message_pending_change: function(event){
+ if (this.get("pending") === 0) {
+ this.$(".o_mail_chat_im_window_header_counter").text("");
+ } else {
+ this.$(".o_mail_chat_im_window_header_counter").text("(" + this.get("pending") + ")");
+ }
},
// utils and event
- get_anonymous_name: function(){
- var name = this.options["defaultUsername"];
- _.each(this.get('session').users, function(u){
- if(!u.id){
- name = u.name;
- }
- });
- return name;
+ _go_bottom: function() {
+ if(this.$(".o_mail_chat_im_window_content").length){
+ this.$(".o_mail_chat_im_window_content").scrollTop(this.$(".o_mail_chat_im_window_content").get(0).scrollHeight);
+ }
+ },
+ focus: function() {
+ this.$(".o_mail_chat_im_window_input").focus();
},
keydown: function(e) {
if(e && e.which == 27) {
- if(this.$el.prev().find('.oe_im_chatview_input').length > 0){
- this.$el.prev().find('.oe_im_chatview_input').focus();
+ if(this.$el.prev().find('.o_mail_chat_im_window_input').length > 0){
+ this.$el.prev().find('.o_mail_chat_im_window_input').focus();
}else{
- this.$el.next().find('.oe_im_chatview_input').focus();
+ this.$el.next().find('.o_mail_chat_im_window_input').focus();
}
e.stopPropagation();
- this.session_update_state('closed');
+ this.session_fold('closed');
}
if(e && e.which !== 13) {
return;
@@ -401,25 +390,17 @@ var Conversation = Widget.extend({
return;
}
this.$("input").val("");
- this.message_send(mes, "message");
+ this.message_send(mes);
},
- _go_bottom: function() {
- this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
+ on_click_header: function(event){
+ this.session_fold();
},
- focus: function() {
- this.$(".oe_im_chatview_input").focus();
- },
- click_header: function(event){
- var classes = event.target.className.split(' ');
- if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){
- this.session_update_state();
- }
- },
- click_close: function(event) {
- this.session_update_state('closed');
+ on_click_close: function(event) {
+ event.stopPropagation(); // avoid triggering event for 'on_click_header'
+ this.session_fold('closed');
},
destroy: function() {
- this.trigger("destroyed", this.get('session').uuid);
+ this.trigger("destroyed", this.get('session').id);
return this._super();
}
});
@@ -430,4 +411,4 @@ return {
ConversationManager: ConversationManager,
};
-}); \ No newline at end of file
+});
diff --git a/addons/mail/static/src/js/mail_chatter.js b/addons/mail/static/src/js/mail_chatter.js
new file mode 100644
index 00000000000..7032f864eb4
--- /dev/null
+++ b/addons/mail/static/src/js/mail_chatter.js
@@ -0,0 +1,471 @@
+odoo.define('mail.chatter', function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var session = require('web.session');
+var web_client = require('web.web_client');
+var Model = require('web.Model');
+var Widget = require('web.Widget');
+var form_common = require('web.form_common');
+var SystrayMenu = require('web.SystrayMenu');
+var mail_utils = require('mail.utils');
+var mail_thread = require('mail.thread');
+
+var QWeb = core.qweb;
+var _t = core._t;
+var internal_bus = core.bus;
+
+
+/**
+ * Mail Compose Message for Chatter
+ *
+ * Extends the basic MailComposeMessage Widget to add 'suggested partner' layer (open popup
+ * when suggested partner is selected without email, or other informations), and the button
+ * to open the 'mail.compose.message' wizard.
+ **/
+var ChatterMailComposeMessage = mail_thread.MailComposeMessage.extend({
+ template: 'mail.chatter.ComposeMessage',
+ init: function(parent, dataset, options){
+ this._super.apply(this, arguments);
+ this.suggested_partners = [];
+ this.context = options.context || {};
+ // options
+ this.options = _.defaults(options, {
+ 'record_name': false,
+ 'is_log': false,
+ 'internal_subtypes': [],
+ });
+ },
+ willStart: function(){
+ if(this.options.is_log){
+ return this._super.apply(this, arguments);
+ }
+ return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients());
+ },
+ start: function(){
+ this.$input = this.$('.o_mail_compose_message_input');
+ this.$input.focus();
+ // add mail compose message full button
+ this.$('.o_mail_compose_message_button_group').append('<button class="btn btn-default o_mail_compose_message_button_full_message" type="button" data-toggle="popover"><i class="fa fa-pencil-square-o"/></button>')
+ this.$('.o_mail_compose_message_button_full_message').on('click', this, this.on_compose_message);
+ return this._super.apply(this, arguments);
+ },
+ message_get_suggested_recipients: function(){
+ var self = this;
+ var email_addresses = _.pluck(this.suggested_partners, 'email_address');
+ return this.thread_dataset.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (suggested_recipients) {
+ var thread_recipients = suggested_recipients[self.context.default_res_id];
+ _.each(thread_recipients, function (recipient) {
+ var parsed_email = mail_utils.parse_email(recipient[1]);
+ if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
+ self.suggested_partners.push({
+ 'checked': true,
+ 'partner_id': recipient[0],
+ 'full_name': recipient[1],
+ 'name': parsed_email[0],
+ 'email_address': parsed_email[1],
+ 'reason': recipient[2],
+ });
+ }
+ });
+ });
+ },
+ /**
+ * Get the list of selected suggested partner
+ * @returns Array() : list of 'recipient' selected partner (may not be created in db)
+ **/
+ get_checked_suggested_partners: function(){
+ var self = this;
+ var checked_partners = [];
+ this.$('.o_mail_chatter_compose_message_suggested_partners input:checked').each(function(index){
+ var $input = $(this);
+ var full_name = $input.data('fullname');
+ checked_partners = checked_partners.concat(_.filter(self.suggested_partners, function(item){
+ return full_name === item.full_name;
+ }));
+ });
+ return checked_partners;
+ },
+ /**
+ * Check the additionnal partners (not necessary registered partners), and open a popup form view
+ * for the ones who informations is missing.
+ * @param Array : list of 'recipient' partners to complete informations or validate
+ * @returns Deferred resolved with the list of checked suggested partners (real partner)
+ **/
+ check_suggested_partners: function (checked_suggested_partners) {
+ var self = this;
+ var check_done = $.Deferred();
+
+ var recipients = _.filter(checked_suggested_partners, function (recipient) { return recipient.checked; });
+ var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); });
+ var names_to_find = _.pluck(recipients_to_find, 'full_name');
+ var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
+ var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
+
+ var names_to_remove = [];
+ var recipient_ids_to_remove = [];
+
+ // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
+ var find_done = $.Deferred();
+ if (names_to_find.length > 0) {
+ find_done = self.thread_dataset.call('message_partner_info_from_emails', [[this.context.default_res_id], names_to_find]);
+ } else {
+ find_done.resolve([]);
+ }
+
+ // for unknown names + incomplete partners -> open popup - cancel = remove from recipients
+ $.when(find_done).pipe(function (result) {
+ var emails_deferred = [];
+ var recipient_popups = result.concat(recipients_to_check);
+
+ _.each(recipient_popups, function (partner_info) {
+ var deferred = $.Deferred();
+ emails_deferred.push(deferred);
+
+ var partner_name = partner_info.full_name;
+ var partner_id = partner_info.partner_id;
+ var parsed_email = mail_utils.parse_email(partner_name);
+
+ var pop = new form_common.FormViewDialog(this, {
+ res_model: 'res.partner',
+ res_id: partner_id,
+ context: {
+ 'force_email': true,
+ 'ref': "compound_context",
+ 'default_name': parsed_email[0],
+ 'default_email': parsed_email[1],
+ },
+ title: _t("Please complete partner's informations"),
+ }).open();
+ pop.on('closed', self, function () {
+ deferred.resolve();
+ });
+ pop.view_form.on('on_button_cancel', self, function () {
+ names_to_remove.push(partner_name);
+ if (partner_id) {
+ recipient_ids_to_remove.push(partner_id);
+ }
+ });
+ });
+ $.when.apply($, emails_deferred).then(function () {
+ var new_names_to_find = _.difference(names_to_find, names_to_remove);
+ find_done = $.Deferred();
+ if (new_names_to_find.length > 0) {
+ find_done = self.thread_dataset.call('message_partner_info_from_emails', [[self.context.default_res_id], new_names_to_find, true]);
+ } else {
+ find_done.resolve([]);
+ }
+ $.when(find_done).pipe(function (result) {
+ var recipient_popups = result.concat(recipients_to_check);
+ _.each(recipient_popups, function (partner_info) {
+ if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
+ recipient_ids.push(partner_info.partner_id);
+ }
+ });
+ }).pipe(function () {
+ check_done.resolve(recipient_ids);
+ });
+ });
+ });
+ return check_done;
+ },
+ message_post: function(body, attachment_ids, values){
+ var self = this;
+ var _super = self._super;
+ // default values
+ values = _.defaults(values || {}, {
+ 'message_type': 'comment',
+ 'content_subtype': 'html',
+ 'partner_ids': [],
+ 'subtype_id': false,
+ 'subtype': false,
+ });
+ // update subtype
+ if(this.options.is_log) {
+ var subtype_id = parseInt(this.$('.o_mail_chatter_compose_message_subtype_select').val());
+ if(_.indexOf(_.pluck(this.internal_subtypes, 'id'), subtype_id) == -1) {
+ values['subtype'] = 'mail.mt_note'
+ }else{
+ values['subtype_id'] = subtype_id;
+ }
+ }else{
+ values['subtype'] = 'mail.mt_comment';
+ }
+ // update partner_ids
+ var def = $.when();
+ if(this.options.is_log){
+ // directly call the super()
+ def = _.bind(_super, self)(body, attachment_ids, values);
+ }else{
+ // call the super() when all partner popup are treated
+ var checked_suggested_partners = this.get_checked_suggested_partners();
+ this.check_suggested_partners(checked_suggested_partners).done(function(partner_ids){
+ // update context
+ values['partner_ids'] = partner_ids;
+ values['context'] = _.defaults(self.context, {
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ });
+ def = _.bind(_super, self)(body, attachment_ids, values);
+ });
+ }
+ return def
+ },
+ is_empty: function(){
+ return this.$input.val() === '';
+ },
+ on_compose_message: function(event){
+ var self = this;
+ if (!this.do_check_attachment_upload()){
+ return false;
+ }
+
+ var recipient_done = $.Deferred();
+ if (this.options.is_log){
+ recipient_done.resolve([]);
+ }else{
+ var checked_suggested_partners = this.get_checked_suggested_partners();
+ recipient_done = this.check_suggested_partners(checked_suggested_partners);
+ }
+ recipient_done.then(function(partner_ids){
+ var context = {
+ 'default_parent_id': self.id,
+ 'default_body': mail_utils.get_text2html(self.$('.o_mail_compose_message_input').val()),
+ 'default_attachment_ids': _.pluck(self.get('attachment_ids'), 'id'),
+ 'default_partner_ids': partner_ids,
+ 'default_is_log': self.options.is_log,
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ };
+
+ if (self.context.default_model && self.context.default_res_id) {
+ context.default_model = self.context.default_model;
+ context.default_res_id = self.context.default_res_id;
+ }
+
+ self.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: context,
+ }, {
+ on_close: function() {
+ self.trigger('need_refresh');
+ },
+ });
+ });
+ }
+});
+
+
+/**
+ * Document Chatter ('mail_thread' widget)
+ *
+ * Displayed on every document. Since it is displayed on a form view, it extends 'AbstractField' widget, and
+ * required the behaviour of 'MailThreadMixin' to manage mail.message (using common methods).
+ * This widget is responsible of the toggle the message composer and the custom message rendering.
+ **/
+var ChatterMailThread = form_common.AbstractField.extend(mail_thread.MailThreadMixin, {
+ template: 'mail.chatter.ChatterMailThread',
+ events: {
+ "click .o_mail_redirect": "on_click_redirect",
+ "click .o_mail_thread_message_star": "on_message_star",
+ "click .o_mail_thread_message_needaction": "on_message_needaction",
+ "click .o_mail_chatter_show_more_message": "on_message_show_more",
+ // toggle message composer (!! declaration order is important !!)
+ "click .o_mail_chatter_button_new_message": "on_open_composer",
+ "click .o_mail_chatter_button_log_note": "on_open_composer",
+ "blur .o_mail_compose_message_input": "on_close_composer",
+ "mousedown .o_mail_chatter_compose_message": function(event) {
+ this.stay_open = true;
+ },
+ "focus .o_mail_compose_message_input": function(event) {
+ this.stay_open = false;
+ },
+ "keydown .o_mail_compose_message_input": function(event) {
+ this.stay_open = false;
+ },
+ },
+ init: function (parent, node){
+ this._super.apply(this, arguments);
+ mail_thread.MailThreadMixin.init.call(this);
+ this.message_composer = undefined;
+ this.record_name = '';
+ this.context = this.options.context || {};
+ // options from 'message_ids' node in field_view_get
+ this.options = _.defaults(this.options, {
+ 'display_log_button': true,
+ 'display_document_link': false,
+ 'internal_subtypes': [],
+ 'emoji_list': [],
+ });
+ this.emoji_set_substitution(this.options.emoji_list);
+ },
+ start: function(){
+ mail_thread.MailThreadMixin.start.call(this);
+ return this._super.apply(this, arguments);
+ },
+ on_message_show_more: function(event){
+ var self = this;
+ this.message_load_history().then(function(messages){
+ if(messages.length < mail_thread.LIMIT_MESSAGE){
+ self.$('.o_mail_chatter_show_more_message').hide();
+ }
+ });
+ },
+ /**
+ * When a message is correctly posted, fetch its data to render it
+ * @param {Number} message_id : the identifier of the new posted message
+ * @returns {Deferred}
+ */
+ on_message_post: function(message_id){
+ var self = this;
+ return this.message_format([message_id]).then(function(messages){
+ self.stay_open = false;
+ self.message_insert(messages);
+ self.on_close_composer();
+ });
+ },
+ /**
+ * The value of the field has change (modification or switch to next document). Form view is
+ * not re-rendered, simply updated. A re-fetch is needed.
+ * @override
+ */
+ render_value: function(){
+ var self = this;
+ // re-rendering for mail composer message
+ this.options.display_show_more_message = this.get_value().length > mail_thread.LIMIT_MESSAGE;
+ this.renderElement();
+ // reset dataset
+ this.ThreadDataset = this.view.dataset;
+ // update context (require for mail.compose.message wizard)
+ this.context = _.extend({
+ 'mail_read_set_read': true, // set messages as read in Chatter TODO JEM : not used anymore
+ 'default_res_id': this.view.datarecord.id || false,
+ 'default_model': this.view.model || false,
+ }, this.options.context || {});
+ // fetch messages of current document
+ this.message_format(this.get_value().slice(0, mail_thread.LIMIT_MESSAGE)).then(function(raw_messages){
+ self._message_replace(self._message_preprocess(raw_messages));
+ self.record_name = _.last(raw_messages) ? _.last(raw_messages).record_name : '';
+ // set the thread as read
+ self.ThreadDataset.call('message_set_read', [[self.view.datarecord.id]]).then(function(message_ids){
+ // decrement the needaction top counter
+ internal_bus.trigger('mail_needaction_done', message_ids.length);
+ });
+ });
+ },
+ // composer toggle
+ on_open_composer: function(event){
+ var self = this;
+ this.$('.o_mail_chatter_compose_buttons').hide();
+ this.$('.o_mail_chatter_compose_message_wrap').show();
+ // destroy existing composer
+ if(this.message_composer){
+ this.message_composer.destroy();
+ }
+ // create the new composer
+ this.message_composer = new ChatterMailComposeMessage(this, this.ThreadDataset, {
+ 'internal_subtypes': this.options.internal_subtypes,
+ 'is_log': this.$(event.currentTarget).hasClass('o_mail_chatter_button_log_note'),
+ 'emoji_list': this.options.emoji_list,
+ 'context': this.context,
+ 'record_name': this.record_name,
+ });
+ this.message_composer.appendTo(this.$('.o_mail_chatter_compose_message_wrap'));
+ this.message_composer.on('message_sent', this, this.on_message_post);
+ this.message_composer.on('need_refresh', this, function(e){
+ self.message_refresh();
+ self.on_close_composer();
+ });
+ },
+ on_close_composer: function(event){
+ if(this.message_composer && this.message_composer.is_empty() && !this.stay_open){
+ this.$('.o_mail_chatter_compose_buttons').show();
+ this.$('.o_mail_chatter_compose_message_wrap').hide();
+ }
+ },
+ /**
+ * Refresh the current message, and set them as current message. Use 'message_fetch', since its
+ * domain don't use 'ids', so if there is new messages, they'll be fetched.
+ * @returns {Deferred}
+ */
+ message_refresh: function(){
+ return this.message_load_new();
+ },
+ /**
+ * Render the messages
+ * @override
+ */
+ message_render: function(){
+ this.$('.o_mail_chatter_messages').html(QWeb.render('mail.chatter.ChatterMailThread.messages', {'widget': this}));
+ },
+ /**
+ * Order the messages
+ * @param {Object[]} messages : messages to order
+ * @override
+ */
+ _message_order: function(messages){
+ var messages = mail_thread.MailThreadMixin._message_order.call(this, messages);
+ return messages.reverse();
+ },
+ /**
+ * Return the message domain for the current document mail thread
+ * @override
+ */
+ get_message_domain: function(){
+ var res_id = this.ThreadDataset.ids[this.ThreadDataset.index];
+ return [['model', '=', this.ThreadDataset.model], ['res_id', '=', res_id]];
+ }
+});
+
+core.form_widget_registry.add('mail_thread', ChatterMailThread);
+
+
+
+/**
+ * Global ComposeMessage Top Button
+ *
+ * Add a link on the top user bar to write a full mail. It opens the form view
+ * of the mail.compose.message (in a modal).
+ */
+var ComposeMessageTopButton = Widget.extend({
+ template:'mail.ComposeMessageTopButton',
+ events: {
+ "click": "on_compose_message",
+ },
+ on_compose_message: function (ev) {
+ ev.preventDefault();
+ var ctx = {};
+ if ($('button.o_timeline_compose_post') && $('button.o_timeline_compose_post').is(":visible") == true &&
+ (this.getParent()).getParent().action_manager.inner_widget.active_view.type == 'form'){
+ ctx = {
+ 'default_res_id': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.datarecord.id,
+ 'default_model': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.model,
+ };
+ }
+ web_client.action_manager.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: ctx,
+ });
+ },
+});
+
+// Put the ComposeMessageTopButton widget in the systray menu
+SystrayMenu.Items.push(ComposeMessageTopButton);
+
+
+return {
+ ChatterMailThread: ChatterMailThread,
+};
+
+});
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 0f2353c729e..124ed218a7e 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -13,9 +13,9 @@ var web_client = require('web.web_client');
var _t = core._t;
var qweb = core.qweb;
-/**
+/**
* ------------------------------------------------------------
- * timeline_followers Widget
+ * Mail Followers Widget
* ------------------------------------------------------------
*
* This widget handles the display of a list of records as a vertical
@@ -27,7 +27,7 @@ var qweb = core.qweb;
var Followers = form_common.AbstractField.extend({
- template: 'mail_followers',
+ template: 'mail.Followers',
init: function() {
this._super.apply(this, arguments);
@@ -40,7 +40,7 @@ var Followers = form_common.AbstractField.extend({
this.value = [];
this.followers = [];
this.data_subtype = {};
-
+
this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
},
@@ -112,7 +112,7 @@ var Followers = form_common.AbstractField.extend({
size: 'small',
title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
buttons: [
- { text: _t("Apply"), click: function() {
+ { text: _t("Apply"), click: function() {
self.do_update_subscription(event, user_pid);
this.parents('.modal').modal('hide');
}},
@@ -236,11 +236,11 @@ var Followers = form_common.AbstractField.extend({
var user_follower = _.filter(this.followers, function (rec) { return rec['is_uid'];});
this.message_is_follower = user_follower.length >= 1;
this.follower_id = this.message_is_follower ? user_follower[0]['id'] : undefined;
- $(qweb.render('mail_followers_add_more', {'widget': self})).appendTo(node_user_list);
+ $(qweb.render('mail.Followers.add_more', {'widget': self})).appendTo(node_user_list);
// truncate number of displayed followers
_(this.followers).each(function (record) {
- $(qweb.render('mail_followers_partner', {
+ $(qweb.render('mail.Followers.partner', {
'record': _.extend(record, {'avatar_url': '/web/image/' + record['res_model'] + '/' + record['res_id'] + '/image_small'}),
'widget': self})
).appendTo(node_user_list);
@@ -279,7 +279,7 @@ var Followers = form_common.AbstractField.extend({
if (user_pid) {
dialog = true;
- }
+ }
else {
var subtype_list_ul = this.$('.o_timeline_subtype_list ul').empty();
if (! this.message_is_follower) {
@@ -302,7 +302,7 @@ var Followers = form_common.AbstractField.extend({
display_subtypes:function (data, dialog) {
var self = this;
var $list = this.$('.o_timeline_subtype_list ul');
-
+
if (_.isEmpty(this.data_subtype)) {
this.data_subtype = data;
@@ -326,7 +326,7 @@ var Followers = form_common.AbstractField.extend({
}
old_parent_model = record.parent_model;
record.followed = record.followed || undefined;
- $(qweb.render('mail_followers_subtype', {'record': record,
+ $(qweb.render('mail.Followers.subtype', {'record': record,
'dialog': dialog}))
.appendTo($list);
});
@@ -336,9 +336,9 @@ var Followers = form_common.AbstractField.extend({
do_follow: function () {
var context = new data.CompoundContext(this.build_context(), {});
this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false);
- this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id],
- [session.uid],
- undefined,
+ this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id],
+ [session.uid],
+ undefined,
context])
.then(this.proxy('read_value'));
@@ -403,9 +403,9 @@ var Followers = form_common.AbstractField.extend({
$(event.target).attr("checked", "checked");
}
else {
- self.$('.o_timeline_subtype_list ul').empty();
+ self.$('.o_timeline_subtype_list ul').empty();
}
- }
+ }
else {
var context = new data.CompoundContext(this.build_context(), {});
return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, undefined, checklist, context])
diff --git a/addons/mail/static/src/js/mail_thread.js b/addons/mail/static/src/js/mail_thread.js
new file mode 100644
index 00000000000..b40e1df5ced
--- /dev/null
+++ b/addons/mail/static/src/js/mail_thread.js
@@ -0,0 +1,430 @@
+odoo.define('mail.thread', function (require) {
+"use strict";
+
+var core = require('web.core');
+var data = require('web.data');
+var time = require('web.time');
+var Widget = require('web.Widget');
+var session = require('web.session');
+var web_client = require('web.web_client');
+var mail_utils = require('mail.utils');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+
+var LIMIT_MESSAGE = 20;
+
+/**
+ * Widget : Input textbox to post messages
+ *
+ * Input with 2 buttons to manage attachment and emoji
+ * - Attachment : upload selected attachment (one at a time) and display them below the textbox. Prevent
+ posting message while uploading
+ * - Emoji : popover exposing emoji list, and append emoji shortcode to the textbox when click on emoji image
+ * It triggers 'message_sent' event with the posted message, when it is sent to the server.
+ */
+var MailComposeMessage = Widget.extend({
+ template: 'mail.ComposeMessage',
+ events: {
+ "keydown .o_mail_compose_message_input": "on_keydown",
+ "change input.oe_form_binary_file": "on_attachment_change",
+ "click .o_mail_compose_message_attachment_list .o_mail_attachment_delete": "on_attachment_delete",
+ "click .o_mail_compose_message_button_attachment": 'on_click_attachment',
+ "click .o_mail_compose_message_button_send": 'message_send',
+ },
+ /**
+ * Contructor
+ * @param {Object} options : the options of the MailComposeMessage
+ * @param {Object[]} [options.emoji_list] : the list of emoji
+ * @param {Object} options.context : the context. It should contains the default_res_id' (id of the current document).
+ * @param {chat|mail} options.display_mode : the 'chat' mode will display an input, when the 'mail' mode will display a texarea.
+ */
+ init: function(parent, dataset, options){
+ this._super.apply(this, arguments);
+ this.thread_dataset = dataset;
+ this.options = _.defaults(options || {}, {
+ 'emoji_list': {},
+ 'context': {},
+ 'display_mode': 'mail',
+ });
+ this.emoji_list = this.options.emoji_list;
+ this.context = this.options.context;
+ // attachment handeling
+ this.AttachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
+ this.fileupload_id = _.uniqueId('o_mail_chat_fileupload');
+ this.set('attachment_ids', []);
+ },
+ start: function(){
+ var self = this;
+ this.$input = this.$('.o_mail_compose_message_input');
+ this.$input.focus();
+ this.$attachment_button = this.$(".o_mail_compose_message_button_attachment");
+ // attachments
+ $(window).on(this.fileupload_id, this.on_attachment_loaded);
+ this.on("change:attachment_ids", this, this.attachment_render);
+ // emoji
+ self.$('.o_mail_compose_message_button_emoji').popover({
+ placement: 'top',
+ content: function(){
+ if(!self.$emoji){ // lazy rendering
+ self.$emoji = $(QWeb.render('mail.ComposeMessage.emoji', {'widget': self}));
+ self.$emoji.find('.o_mail_compose_message_emoji_img').on('click', self, self.on_click_emoji_img);
+ }
+ return self.$emoji;
+ },
+ html: true,
+ container: '.o_mail_compose_message_emoji',
+ trigger: 'focus',
+ });
+ return this._super();
+ },
+ // events
+ on_click_attachment: function(event){
+ event.preventDefault();
+ this.$('input.oe_form_binary_file').click();
+ },
+ on_click_emoji_img: function(event){
+ this.$input.val(this.$input.val() + " " + $(event.currentTarget).data('emoji')+ " ");
+ this.$input.focus();
+ },
+ on_keydown: function(event){
+ if(event.which === 13 && this.options.display_mode === 'chat') {
+ this.message_send();
+ }
+ },
+ message_send: function(){
+ var $input = this.$('.o_mail_compose_message_input');
+ var mes = mail_utils.get_text2html($input.val());
+ if (! mes.trim() && this.do_check_attachment_upload()) {
+ return;
+ }
+ $input.val("");
+ this.message_post(mes, _.pluck(this.get('attachment_ids'), 'id'));
+ },
+ /**
+ * Sent the message to the server to create a mail.message, using 'message_post' method
+ * @param {String} body : the core of the message (html, or plaintext)
+ * @param {Number[]} attachment_ids : attachment_ids linked to the message
+ * @param {Object} kwargs : other parameters (in a dictionnary)
+ * @returns {Deferred} resolved when the message is posted, and returning the new message id
+ */
+ message_post: function(body, attachment_ids, kwargs){
+ var self = this;
+ var values = _.defaults(kwargs || {}, {
+ 'body': body,
+ 'attachment_ids': attachment_ids || [],
+ 'message_type': 'comment',
+ 'content_subtype': 'html',
+ 'partner_ids': [],
+ 'subtype': 'mail.mt_comment',
+ });
+ return this.thread_dataset._model.call('message_post', [this.context.default_res_id], values).then(function(message_id){
+ self.clean_attachments(); // empty attachment list
+ self.trigger('message_sent', message_id);
+ return message_id;
+ });
+ },
+ // attachment business
+ on_attachment_change: function(event){
+ var $target = $(event.target);
+ if ($target.val() !== '') {
+ var filename = $target.val().replace(/.*[\\\/]/,'');
+ // if the files exits for this answer, delete the file before upload
+ var attachments = [];
+ for (var i in this.get('attachment_ids')) {
+ if ((this.get('attachment_ids')[i].filename || this.get('attachment_ids')[i].name) == filename) {
+ if (this.get('attachment_ids')[i].upload) {
+ return false;
+ }
+ this.AttachmentDataSet.unlink([this.get('attachment_ids')[i].id]);
+ } else {
+ attachments.push(this.get('attachment_ids')[i]);
+ }
+ }
+ // submit filename
+ this.$('form.oe_form_binary_form').submit();
+ this.$attachment_button.prop('disabled', true);
+
+ attachments.push({
+ 'id': 0,
+ 'name': filename,
+ 'filename': filename,
+ 'url': '',
+ 'upload': true
+ });
+ this.set('attachment_ids', attachments);
+ }
+ },
+ on_attachment_loaded: function(event, result){
+ var attachment_ids = [];
+ if (result.error || !result.id ) {
+ this.do_warn(result.error);
+ attachment_ids = _.filter(this.get('attachment_ids'), function (val) { return !val.upload; });
+ }else{
+ _.each(this.get('attachment_ids'), function(a){
+ if (a.filename == result.filename && a.upload) {
+ attachment_ids.push({
+ 'id': result.id,
+ 'name': result.name || result.filename,
+ 'filename': result.filename,
+ 'url': session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name', 'id': result.id}),
+ });
+ }else{
+ attachment_ids.push(a);
+ }
+ });
+ }
+ this.set('attachment_ids', attachment_ids);
+
+ // TODO JEM : understand the 2 lines below ....
+ var $input = this.$('input.oe_form_binary_file');
+ $input.after($input.clone(true)).remove();
+
+ this.$attachment_button.prop('disabled', false);
+ },
+ on_attachment_delete: function(event){
+ event.stopPropagation();
+ var self = this;
+ var attachment_id = $(event.target).data("id");
+ if (attachment_id) {
+ var attachments = [];
+ _.each(this.get('attachment_ids'), function(attachment){
+ if (attachment_id != attachment.id) {
+ attachments.push(attachment);
+ } else {
+ self.AttachmentDataSet.unlink([attachment_id]);
+ }
+ });
+ this.set('attachment_ids', attachments);
+ }
+ },
+ do_check_attachment_upload: function () {
+ if (_.find(this.get('attachment_ids'), function (file) {return file.upload;})) {
+ this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
+ return false;
+ }
+ return true;
+ },
+ clean_attachments: function(){
+ this.set('attachment_ids', []);
+ },
+ // ui
+ attachment_render: function(){
+ this.$('.o_mail_compose_message_attachment_list').html(QWeb.render('mail.ComposeMessage.attachments', {'widget': this}));
+ },
+ focus: function(){
+ this.$input.focus();
+ }
+});
+
+
+/**
+ * Mail Thread Mixin : Messages Managment
+ *
+ * Load, Fetch, Display mail.message
+ * This is a mixin since it will be inherit by a form_common.AbstractField and a Wigdet (Client Action)
+ *
+ * @mixin
+ **/
+var MailThreadMixin = {
+ /**
+ * Constructor of the 'mail_thread' mixin
+ * @param {Boolean} [options.display_log_button] : display of the 'log a note' button
+ * @param {Boolean} [options.display_document_link] : display document link on message
+ * @param {Boolean} [options.display_needaction_button] : display the needaction 'check' button on messages
+ * @param {Object[]} [options.internal_subtypes]: list of internal subtype (for employee only)
+ * @param {Object[]} [options.emoji_list] : list of emoji
+ */
+ init: function(){
+ this.MessageDatasetSearch = new data.DataSetSearch(this, 'mail.message');
+ this.set('messages', []);
+ this.emoji_substitution = {};
+ // options
+ this.options = _.defaults(this.options || {}, {
+ 'display_log_button': false,
+ 'display_document_link': false,
+ 'display_needaction_button': false,
+ 'internal_subtypes': [],
+ 'emoji_list': [],
+ });
+ },
+ start: function(){
+ this.on("change:messages", this, this.message_render);
+ },
+ // Common Actions (They should be bind on the implementing widget, the 'events' dict)
+ /**
+ * Generic redirect action : redirect to the form view if the
+ * click node contains 'data-oe-model' and 'data-oe-id'.
+ */
+ on_click_redirect: function(event){
+ event.preventDefault();
+ var res_id = $(event.target).data('oe-id');
+ var res_model = $(event.target).data('oe-model');
+ web_client.action_manager.do_push_state({
+ 'model': res_model,
+ 'id': res_id,
+ 'title': this.record_name,
+ });
+ this.do_action({
+ type:'ir.actions.act_window',
+ view_type: 'form',
+ view_mode: 'form',
+ res_model: res_model,
+ views: [[false, 'form']],
+ res_id: res_id,
+ }, {
+ 'on_reverse_breadcrumb': this.on_reverse_breadcrumb,
+ });
+ },
+ /**
+ * Will be called when clicking on the Breadcrumb (mainly of client action)
+ * Need to be override
+ */
+ on_reverse_breadcrumb: function(){
+
+ },
+ /**
+ * Toggle a message as 'starred' (or not), by toggling the 'o_mail_message_starred' css class.
+ * Trigger when clicking on selector '.o_mail_thread_message_star'.
+ */
+ on_message_star: function(event){
+ var $source = this.$(event.currentTarget);
+ var mid = $source.data('message-id');
+ var is_starred = !$source.hasClass('o_mail_message_starred');
+ return this.MessageDatasetSearch.call('set_message_starred', [[mid], is_starred]).then(function(res){
+ $source.toggleClass('o_mail_message_starred');
+ return mid;
+ });
+ },
+ on_message_needaction: function(event){
+ var $source = this.$(event.currentTarget);
+ var mid = $source.data('message-id');
+ return this.MessageDatasetSearch.call('set_message_done', [[mid]]).then(function(res){
+ $source.remove();
+ return mid;
+ });
+ },
+ // Message functions
+ /**
+ * Fetch given message
+ * @param {Number[]} message_ids : list of mail.message identifiers to fetch
+ * @returns {Deferred} resolved when the messages are loaded
+ */
+ message_format: function(message_ids){
+ return this.MessageDatasetSearch._model.call('message_format', [message_ids]);
+ },
+ /**
+ * Fetch mail.message in the format defined server side
+ * @param {Array} domain : Odoo Domain of the message to fetch
+ * @param {Number} limit : the limit of messages to fetch
+ * @returns {Deferred} resolved when the messages are loaded
+ */
+ message_fetch: function(domain, limit){
+ domain = domain || this.get_message_domain();
+ limit = limit || LIMIT_MESSAGE;
+ return this.MessageDatasetSearch._model.call('message_fetch', [domain], {limit: limit});
+ },
+ _message_replace: function(messages){
+ this.set('messages', this._message_order(messages));
+ },
+ /**
+ * Order the message in a particular order, to be defined by implementation
+ * @param {Object[]} messages : messages to order
+ */
+ _message_order: function(messages){
+ return _.sortBy(messages, 'date');
+ },
+ /**
+ * Insert messages in the current list
+ * @param {Object[]} messages : list of mail.message (formatted server side)
+ */
+ message_insert: function(raw_messages){
+ var current_messages = this.get('messages');
+ current_messages = current_messages.concat(this._message_preprocess(raw_messages));
+ this._message_replace(current_messages);
+ },
+ /**
+ * Load history of message, with the 'history' domain, and insert them in the current list
+ * @returns {Deferred} resolved when the messages are fetched and returning the raw
+ * messages (formatted server side)
+ */
+ message_load_history: function(){
+ var self = this;
+ return this.message_fetch(this.get_message_domain_history()).then(function(raw_messages){
+ self.message_insert(raw_messages);
+ return raw_messages
+ });
+ },
+ /**
+ * Load messages according to the current domain, and set that new list as current messages,
+ * erasing the old ones.
+ * @returns {Deferred} resolved when the messages are fetched and returning the raw
+ * messages (formatted server side)
+ */
+ message_load_new: function(){
+ var self = this;
+ return this.message_fetch(this.get_message_domain()).then(function(raw_messages){
+ self._message_replace(self._message_preprocess(raw_messages));
+ return raw_messages
+ });
+ },
+ /**
+ * Preprocess the list of messages before rendering, add 'computed' field (is_needaction,
+ * is_starred, ...), and apply image shortcode to the message body.
+ * @param {Object[]} messages : list of raw mail.message (formatted server side)
+ * @returns list of messages. It can be sorted, grouped, ...
+ */
+ _message_preprocess: function(messages){
+ var self = this;
+ _.each(messages, function(m){
+ m.is_needaction = _.contains(m.needaction_partner_ids, session.partner_id);
+ m.is_starred = _.contains(m.starred_partner_ids, session.partner_id);
+ m.date = moment(time.str_to_datetime(m.date)).format('YYYY-MM-DD HH:mm:ss'); // set the date in the correct browser user timezone
+ if(m.body){
+ m.body = mail_utils.shortcode_apply(m.body, self.emoji_substitution);
+ }
+ _.each(m.attachment_ids, function(a){
+ a.url = mail_utils.get_attachment_url(session, m.id, a.id);
+ });
+ });
+ return _.sortBy(messages, 'date');
+ },
+ /**
+ * Take the current messages, render them, and insert the rendering in the DOM.
+ * This is triggered when the mesasge list change.
+ * Must be redefined, since it depends on the complete DOM widget
+ */
+ message_render: function(){
+
+ },
+ // Message Domains
+ /**
+ * Return the current domain to fetch the message of the thread. this should
+ * be redifined by the thread implementation.
+ */
+ get_message_domain: function(){
+ return [];
+ },
+ get_message_domain_history: function(){
+ return this.get_message_domain().concat([['id', '<', _.min(_.pluck(this.get('messages'), 'id'))]]);
+ },
+ // Others
+ /**
+ * Set the list of emoji to be substituted in message body
+ * @param {Object[]} emoji_list : list of emoji Object
+ */
+ emoji_set_substitution: function(emoji_list){
+ this.emoji_substitution = mail_utils.shortcode_substitution(emoji_list);
+ },
+};
+
+
+return {
+ MailComposeMessage: MailComposeMessage,
+ MailThreadMixin: MailThreadMixin,
+ LIMIT_MESSAGE: LIMIT_MESSAGE,
+}
+
+
+});
diff --git a/addons/mail/static/src/js/mail_utils.js b/addons/mail/static/src/js/mail_utils.js
index 656dc941a75..a18dc449d6b 100644
--- a/addons/mail/static/src/js/mail_utils.js
+++ b/addons/mail/static/src/js/mail_utils.js
@@ -5,7 +5,7 @@ odoo.define('mail.utils', function () {
* ------------------------------------------------------------
* ChatterUtils
* ------------------------------------------------------------
- *
+ *
* This class holds a few tools method for Chatter.
* Some regular expressions not used anymore, kept because I want to
* - (^|\s)@((\w|@|\.)*): @login@log.log
@@ -28,24 +28,6 @@ function parse_email(text) {
}
/**
- * Replaces some expressions
- * - :name - shortcut to an image
- */
-function do_replace_expressions(string) {
- var icon_list = ['al', 'pinky'];
- /* special shortcut: :name, try to find an icon if in list */
- var regex_login = new RegExp(/(^|\s):((\w)*)/g);
- var regex_res = regex_login.exec(string);
- while (regex_res !== null) {
- var icon_name = regex_res[2];
- if (_.include(icon_list, icon_name))
- string = string.replace(regex_res[0], regex_res[1] + '<img src="/mail/static/src/img/_' + icon_name + '.png" width="22px" height="22px" alt="' + icon_name + '"/>');
- regex_res = regex_login.exec(string);
- }
- return string;
-}
-
-/**
* Replaces textarea text into html text (add <p>, <a>)
* TDE note : should be done server-side, in Python -> use mail.compose.message ?
*/
@@ -55,7 +37,7 @@ function get_text2html(text) {
.replace(/[\n\r]/g,'<br/>');
}
-/* Returns the complete domain with "&"
+/* Returns the complete domain with "&"
* TDE note: please add some comments to explain how/why
*/
function expand_domain(domain) {
@@ -90,6 +72,8 @@ function breakword(str){
return out;
}
+
+// TODO JEM : remove me in master
function bindTooltipTo($el, value, position) {
$el.tooltip({
'title': value,
@@ -106,13 +90,92 @@ function bindTooltipTo($el, value, position) {
});
}
+
+/**
+ * ------------------------------------------------------------
+ * MailChat Utils
+ * ------------------------------------------------------------
+ */
+
+ /**
+ * Apply the given shortcode to the 'str' message.
+ * @param str : the text to substitute (html).
+ * @param shortcodes : dict where key is the shortcode, and the value is the substitution
+ */
+function shortcode_apply(str, shortcodes){
+ var re_escape = function(str){
+ return String(str).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
+ };
+ _.each(_.keys(shortcodes), function(key){
+ str = str.replace( new RegExp("(?:^|\\s|<[a-z]*>)(" + re_escape(key) + ")(?:\\s|$|</[a-z]*>)"), ' <span class="o_mail_emoji">'+shortcodes[key]+'</span> ');
+ });
+ return str;
+}
+
+/**
+ * Transform the list of emoji (shortcode Object) into a key-array representing
+ * the substitution to apply
+ * @param {Ojbect[]} emoji_list : list of emoji object
+ * @returns {Object} mapping between the code (as key) and the substitution (as value)
+ */
+function shortcode_substitution(emoji_list){
+ var emoji_substitution = {};
+ _.each(emoji_list, function(emoji){
+ emoji_substitution[emoji.source] = emoji.substitution;
+ });
+ return emoji_substitution;
+}
+
+/**
+ * Associate a font awesome class to a attachment file type
+ * @param {string} file_type : file type to analyse
+ * @returns {string} the fa class associated
+ */
+function attachment_filetype_to_fa_class(file_type){
+ var mapping = {
+ 'text': 'fa fa-file-text-o',
+ 'document': 'fa fa-file-word-o',
+ 'audio': 'fa fa-file-audio-o',
+ 'archive': 'fa fa-file-archive-o',
+ 'disk': 'fa fa-file-archive-o',
+ 'image': 'fa fa-file-photo-o',
+ 'webimage': 'fa fa-file-photo-o',
+ 'script': 'fa fa-file-code-o',
+ 'html': 'fa fa-file-code-o',
+ 'video': 'fa fa-file-movie-o',
+ 'spreadsheet': 'fa fa-file-excel-o',
+ 'print': 'fa fa-file-pdf-o',
+ 'presentation': 'fa fa-file-powerpoint-o',
+ }
+ if(_.contains(_.keys(mapping), file_type)){
+ return mapping[file_type];
+ }
+ return 'fa fa-file-o';
+}
+
+/**
+ * Play the noise 'ting' to notify user
+ */
+function beep(session){
+ if (typeof(Audio) === "undefined") {
+ return;
+ }
+ var audio = new Audio();
+ var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
+ audio.src = session.url("/mail/static/src/audio/ting") + ext;
+ audio.play();
+}
+
return {
parse_email: parse_email,
- do_replace_expressions: do_replace_expressions,
get_text2html: get_text2html,
expand_domain: expand_domain,
breakword: breakword,
bindTooltipTo: bindTooltipTo,
+ shortcode_apply: shortcode_apply,
+ shortcode_substitution: shortcode_substitution,
+ attachment_filetype_to_fa_class: attachment_filetype_to_fa_class,
+ beep: beep,
};
});
diff --git a/addons/mail/static/src/less/mail_chat_backend.less b/addons/mail/static/src/less/mail_chat_backend.less
new file mode 100644
index 00000000000..80fe41c541c
--- /dev/null
+++ b/addons/mail/static/src/less/mail_chat_backend.less
@@ -0,0 +1,212 @@
+
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+.o_mail_chat {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 100%;
+
+ @media (max-width: @screen-xs-max) {
+ position: static;
+ height: auto;
+ }
+
+ .o-flex-display();
+ .o-flex-flow(row, nowrap);
+
+ .o_mail_chat_sidebar {
+ .o-flex(0, 0, 15%);
+ overflow: auto;
+ }
+
+ .o_mail_chat_content {
+ .o-flex(0, 0, 85%);
+ height: 100%;
+ .o-flex-display();
+ .o-flex-flow(column, nowrap);
+ @media (max-width: @screen-xs-max) {
+ height: auto;
+ }
+
+ .o_mail_chat_messages {
+ .o-flex(1, 0, 0);
+ overflow: auto;
+ }
+
+ .o_mail_chat_composer_wrapper {
+ .o-flex(0, 0, auto);
+ .o-flex-display();
+ /*
+ > img {
+ .o-flex(0, 0, auto);
+ width: 64px;
+ height: 64px;
+ }
+
+ > input {
+ .o-flex(1, 0, auto);
+ }
+ */
+ }
+ }
+}
+
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+.o_mail_chat {
+ .o_mail_chat_sidebar {
+ background-color: @gray;
+ padding: 1%;
+
+ > .nav-pills {
+ > li {
+ > a {
+ color: @odoo-brand-optional; // community compatibility: override base.css
+ &:hover, &:focus {
+ background-color: @gray-dark;
+ }
+ &.active {
+ background-color: @gray-dark;
+ }
+ }
+
+ &.o_mail_chat_channel_item, &.o_mail_chat_partner_item {
+ > a {
+ color: @gray-lighter;
+ > .badge {
+ color: white;
+ background-color: @odoo-brand-optional;
+ }
+
+ > i.fa {
+ padding-top: 3px;
+ padding-right: 3px;
+
+ &.o_online {
+ color: @odoo-brand-optional;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .o_mail_chat_channel_item.o_mail_chat_channel_unread, .o_mail_chat_partner_item.o_mail_chat_channel_unread{
+ font-weight: bold;
+ }
+
+ .o_mail_chat_sidebar_title {
+ text-transform: uppercase;
+ color: @gray-lighter;
+ margin-top: 25px;
+ .user-select(none);
+ cursor: default;
+ }
+
+ .o_mail_chat_add_more_search{
+ padding: @padding-base-vertical @padding-base-horizontal;
+ .o_mail_chat_add_more_text{
+ color: @gray-lighter;
+ display: inline-block;
+ font-style: italic;
+ cursor: pointer;
+ font-size: @font-size-base;
+ }
+ .o_mail_chat_add_more_text:hover{
+ text-decoration: underline;
+ color: white;
+ cursor: pointer;
+ }
+ }
+
+ }
+
+ .o_mail_chat_content {
+ background-color: white;
+
+ .o_mail_chat_help_message{
+ padding: 25px;
+ color: @gray-light;
+ }
+
+ .o_mail_thread_date_separator{
+ margin: auto;
+ width: 90%;
+ .o_mail_thread_date_wrapper {
+ position: relative;
+ height: 20px;
+ border-bottom: solid WhiteSmoke 2px;
+ margin-bottom: 15px;
+ .o_mail_thread_date_separator_title {
+ display: table;
+ position: relative;
+ top: 10px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ padding: 0 10px 0 10px;
+ font-weight: bold;
+ background: white;
+ }
+ }
+ }
+
+ .o_mail_thread_message{
+ //border-bottom: solid WhiteSmoke 1px;
+ }
+ /*
+ .o_mail_chat_messages {
+ .o_slack_message {
+ .o_slack_message_avatar {
+ > img {
+
+ }
+ }
+ .o_slack_message_content {
+ .o_slack_message_author {
+ font-weight: bold;
+ .o_slack_message_time {
+ font-weight: normal;
+ color: @gray-light;
+ }
+ }
+
+ }
+ }
+ }
+ */
+
+ .o_mail_chat_composer_wrapper {
+ border-top: 1px solid @gray-lighter;
+ background-color: @gray-lighter;
+ padding: 10px;
+ .o_mail_compose_message{
+ margin-bottom: 0px;
+ }
+ /*
+ > img {
+ background-image: url(/web/static/src/img/placeholder.png);
+ }
+ > input {
+ padding-left: 15px;
+ resize: none;
+ border: 0;
+ outline: none;
+ }
+ */
+ }
+ }
+}
+
+
+// ------------------------------------------------------------------
+// Other components
+// ------------------------------------------------------------------
+.o_mail_chat_partner_invite{
+ height: 50px;
+}
diff --git a/addons/mail/static/src/less/mail_chatter.less b/addons/mail/static/src/less/mail_chatter.less
new file mode 100644
index 00000000000..1f7e9b29018
--- /dev/null
+++ b/addons/mail/static/src/less/mail_chatter.less
@@ -0,0 +1,54 @@
+
+.o_mail_chatter{
+ /* Layout for chatter header (buttons and compose message) */
+ .o_mail_chatter_compose_buttons, .o_mail_chatter_compose_message_wrap{
+ padding-left:15px;
+ margin-bottom:20px;
+ //background-color: lightGrey;
+ }
+
+ /* Specific message style for chatter */
+ .o_mail_chatter_messages{
+
+ .o_mail_thread_message{
+ .o_mail_thread_message_type_comment{ // message comment
+ &:not(.o_mail_thread_message_subtype_note){
+ background-color: white;
+ box-shadow: 0px 1px #CACACA;
+ }
+ }
+ .o_mail_thread_message_type_email{ // message email
+ background-color: white;
+ box-shadow: 0px 1px #CACACA;
+ }
+ .o_mail_thread_message_type_notification{ // message notification
+
+ }
+ }
+
+ .o_mail_thread_date_separator{
+ .o_mail_thread_date_wrapper {
+ height: 20px;
+ .o_mail_thread_date_separator_title {
+ text-align: center;
+ font-weight: bold;
+ color: @gray-light;
+ }
+ }
+ }
+ }
+ .o_mail_chatter_show_more_message{
+ text-align: center;
+ cursor: pointer;
+ }
+
+}
+
+
+.o_mail_chatter_compose_message{
+ .o_mail_chatter_compose_message_suggested_partners label{
+ vertical-align: middle;
+ display: block;
+ line-height: 14px;
+ }
+} \ No newline at end of file
diff --git a/addons/mail/static/src/less/mail_thread.less b/addons/mail/static/src/less/mail_thread.less
new file mode 100644
index 00000000000..15cfc5cdf94
--- /dev/null
+++ b/addons/mail/static/src/less/mail_thread.less
@@ -0,0 +1,184 @@
+// ------------------------------------------------------------------
+// Mail Thread 'Mixin'
+// ------------------------------------------------------------------
+.o_mail_thread{
+
+ /* Starred and Needaction on message */
+ .o_mail_thread_message_star, .o_mail_thread_message_needaction {
+ opacity: 0.5;
+ color: black;
+ }
+ .o_mail_thread_message_star:hover, .o_mail_thread_message_needaction:hover{
+ opacity: 1;
+ cursor: pointer;
+ }
+ .o_mail_thread_message_star.o_mail_message_starred{
+ color: orange;
+ }
+ /* author avatar */
+ .o_mail_thread_avatar{
+ width: 4.5em;
+ max-width: 50px;
+ }
+ /* message layout */
+ .o_mail_thread_message{
+ margin-bottom: 10px;
+ }
+ .o_mail_thread_message_avatar{
+ text-align: center;
+ }
+ .o_mail_thread_message_core{
+ padding: 5px;
+ }
+ .o_mail_thread_message_info{
+ margin-top: 0;
+ }
+
+ /* mail redirect */
+ .o_mail_redirect{
+ cursor: pointer;
+ }
+}
+
+// ------------------------------------------------------------------
+// Mail Compose Message
+// ------------------------------------------------------------------
+.o_mail_compose_message{
+
+ // Emoji size in the popover
+ .o_mail_compose_message_emoji_img img{
+ height: 26px;
+ }
+
+ .o_mail_compose_message_mode_mail{
+ textarea {
+ resize: vertical;
+ width: 100%;
+ }
+ }
+}
+
+// ------------------------------------------------------------------
+// Attachment Icons (common for mail thread message and compose message)
+// ------------------------------------------------------------------
+
+@-webkit-keyframes oe_mail_attach_loading_anim {
+ 0% { background: #4BBD00 }
+ 50% { background: #009123 }
+ 100% { background: #4BBD00 }
+}
+@-moz-keyframes oe_mail_attach_loading_anim {
+ 0% { background: #4BBD00 }
+ 50% { background: #009123 }
+ 100% { background: #4BBD00 }
+}
+@-o-keyframes oe_mail_attach_loading_anim {
+ 0% { background: #4BBD00 }
+ 50% { background: #009123 }
+ 100% { background: #4BBD00 }
+}
+@keyframes oe_mail_attach_loading_anim {
+ 0% { background: #4BBD00 }
+ 50% { background: #009123 }
+ 100% { background: #4BBD00 }
+}
+
+.o_mail_attachment{
+ display: inline-block;
+ width: 100px;
+ margin: 4px 2px;
+ min-height: 80px;
+ position: relative;
+ border-radius: 3px;
+ text-align: center;
+ vertical-align: top;
+
+ img {
+ position: absolute;
+ width: 48px;
+ height: 48px;
+ top: 0px;
+ left: 50%;
+ margin-left: -24px;
+ }
+
+ .o_mail_attachment_name {
+ display: inline-block;
+ max-width: 100%;
+ padding: 1px 3px;
+ margin-top: 50px;
+ margin-bottom: 5px;
+ background: #F2F7FF;
+ overflow: hidden;
+ color: #4c4c4c;
+ text-shadow: none;
+ border-radius: 3px;
+ }
+
+ &.o_mail_attachment_preview {
+ background: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVQYV2MsLS39z4AGLCws0IUYGIeCwrVr12J45sSJE5ieGQIKAbuZKf/EMCs7AAAAAElFTkSuQmCC );
+ }
+
+ &.o_mail_attachment_preview img {
+ display: block;
+ position: relative;
+ margin:0px;
+ width: 100px;
+ height: 80px;
+ border-radius: 1px;
+ border: solid 3px #FFF;
+ margin-left: -50px;
+ box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.19);
+ }
+
+ .o_mail_attachment_progress_bar {
+ display: none;
+ position: absolute;
+ top: 18px;
+ left: 16px;
+ right: 16px;
+ height: 17px;
+ line-height: 13px;
+ padding: 0px;
+ background: #4BBD00;
+ color: white;
+ text-align: center;
+ border-radius: 3px;
+ border: solid 1px rgba(0,0,0,0.2);
+ box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.34);
+ -webkit-animation: oe_mail_attach_loading_anim 0.75s infinite linear;
+ -moz-animation: oe_mail_attach_loading_anim 0.75s infinite linear;
+ -o-animation: oe_mail_attach_loading_anim 0.75s infinite linear;
+ animation: oe_mail_attach_loading_anim 0.75s infinite linear;
+ }
+
+ &.o_mail_uploading .o_mail_attachment_progress_bar {
+ display: block;
+ }
+ .o_mail_attachment_delete .fa {
+ display: none;
+ }
+
+ .o_mail_attachment_delete .fa {
+ display: block;
+ position: absolute;
+ top: -7px;
+ right: 0px;
+ color: black;
+ text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white;
+ cursor: pointer;
+ opacity: 0;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
+ }
+
+ &:hover .o_mail_attachment_delete .fa {
+ opacity: 1;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ -o-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
+ }
+}
diff --git a/addons/mail/static/src/xml/mail_chat_backend.xml b/addons/mail/static/src/xml/mail_chat_backend.xml
new file mode 100644
index 00000000000..9dc42749767
--- /dev/null
+++ b/addons/mail/static/src/xml/mail_chat_backend.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <!-- Notification Counter of the Systray Menu -->
+ <t t-name="mail.chat.NotificationTopButton">
+ <li t-att-title='_t("Notifications")'>
+ <a href="#">
+ <i class="fa fa-comment"><t t-esc="widget.get('counter') || ''"/></i>
+ </a>
+ </li>
+ </t>
+
+ <!-- Add More / Search Box -->
+ <t t-name="mail.chat.AbstractAddMoreSearch">
+ <div class="o_mail_chat_add_more_search">
+ <a href="#" class="o_mail_chat_add_more_text"><t t-esc="widget.label"/></a>
+ <div class="o_mail_chat_add_more_search_bar" style="display: none;">
+ <input type="text" class="o_mail_chat_search_input"/>
+ </div>
+ </div>
+ </t>
+
+ <!-- PartnerInviteDialog : content of the dialog -->
+ <t t-name="mail.chat.PartnerInviteDialog">
+ <div class="o_mail_chat_partner_invite">
+ <input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
+ </div>
+ </t>
+
+
+ <!--
+ ChatMailThread
+ Layout of the client action
+ param: widget : the widget (ChatMailThread)
+ -->
+ <t t-name="mail.chat.ChatMailThread">
+ <div class="o_mail_chat o_mail_thread">
+ <!-- Sidebar -->
+ <div class="o_mail_chat_sidebar">
+ <!-- Static channel list -->
+ <ul class="nav nav-pills nav-stacked">
+ <li class="o_mail_chat_channel_item" data-channel-id="channel_inbox">
+ <a href="#">@ Inbox
+ <span class="badge pull-right o_mail_chat_needaction" data-channel-id="channel_inbox" t-att-style="((widget.get('needaction_inbox_counter') &gt; 0) ? 'display:block;' : 'display:none;')">
+ <t t-esc="widget.get('needaction_inbox_counter')"/>
+ </span>
+ </a>
+ </li>
+ <li class="o_mail_chat_channel_item" data-channel-id="channel_starred">
+ <a href="#"><i class="fa fa-star-o"> Starred</i></a>
+ </li>
+ </ul>
+ <!-- Channel list -->
+ <h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Channels')"/></h4>
+ <t t-call="mail.chat.ChatMailThread.channels">
+ <t t-set="channel_slot" t-value="'channel_channel'"/>
+ </t>
+ <!-- Direct Message list (res.users)-->
+ <h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Direct Message')"/></h4>
+ <t t-call="mail.chat.ChatMailThread.partners"/>
+ <!-- Private Group list -->
+ <h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Private Group')"/></h4>
+ <t t-call="mail.chat.ChatMailThread.channels">
+ <t t-set="channel_slot" t-value="'channel_private_group'"/>
+ </t>
+ </div>
+ <!-- Content -->
+ <div class="o_mail_chat_content">
+ <div class="o_mail_chat_messages">
+ <div class="container-fluid o_mail_chat_messages_content">
+ <t t-call="mail.chat.ChatMailThread.content"/>
+ </div>
+ </div>
+ <div class="o_mail_chat_composer_wrapper">
+
+ </div>
+ </div>
+ </div>
+ </t>
+
+ <!--
+ List of channel in the sidebar
+ param: channel_slot : the name of the channel slot
+ param: widget : the widget (ChatMailThread)
+ -->
+ <t t-name="mail.chat.ChatMailThread.channels">
+ <ul t-att-class="'nav nav-pills nav-stacked o_mail_chat_channel_slot_' + channel_slot" data-toggle="pill">
+ <t t-foreach="widget.get(channel_slot)" t-as="channel">
+ <li t-att-class="'o_mail_chat_channel_item ' + (channel.message_needaction_counter &gt; 0 ? 'o_mail_chat_channel_unread' : ' ')" t-att-data-channel-id="channel.id" role="presentation">
+ <a t-att-href="_.str.sprintf('#active_id=%s&amp;action=%s', channel.id, widget.action.id)">
+ <t t-esc="channel.name"/>
+ <span class="badge pull-right o_mail_chat_needaction" t-att-data-channel-id="channel.id" t-att-style="((channel.message_needaction_counter &gt; 0) ? 'display:block;' : 'display:none;')">
+ <t t-esc="channel.message_needaction_counter"/>
+ </span>
+ </a>
+ </li>
+ </t>
+ </ul>
+ </t>
+
+ <!--
+ List of Partners of Direct Message channel
+ param: widget : the widget (ChatMailThread)
+ -->
+ <t t-name="mail.chat.ChatMailThread.partners">
+ <ul class="nav nav-pills nav-stacked o_mail_chat_channel_slot_partners">
+ <t t-foreach="widget.get('partners')" t-as="partner">
+ <li class="o_mail_chat_partner_item" t-att-data-partner-id="partner.id" role="presentation">
+ <a href="#">
+ <i t-att-class="'fa ' + (partner.im_status == 'online' ? 'fa-circle' : 'fa-circle-o')"/>
+ <t t-esc="partner.name"/>
+ <span class="fa fa-times pull-right o_mail_chat_partner_unpin" t-att-title="_t('Untoggle')" t-att-data-partner-id="partner.id"/>
+ </a>
+ </li>
+ </t>
+ </ul>
+ </t>
+
+ <!--
+ Channel content (messages) or 'Help Message' of the client action
+ param: widget : the widget (ChatMailThread)
+ -->
+ <t t-name="mail.chat.ChatMailThread.content">
+ <t t-if="widget.get('messages').length">
+ <t t-call="mail.Thread.messages"/>
+ </t>
+ <t t-if="!widget.get('messages').length">
+ <div class="o_mail_chat_help_message">
+ <t t-raw="widget.help_message"/>
+ </div>
+ </t>
+ </t>
+
+ <!--
+ Buttons of the Control Panel
+ -->
+ <t t-name="mail.chat.ControlButtons">
+ <div class="o_mail_chat_cp_buttons">
+ <button type="button" class="btn btn-primary btn-sm o_mail_chat_button_invite" t-att-title='_t("Invite people")'>Invite</button>
+ <button type="button" class="btn btn-default btn-sm o_mail_chat_button_minimize" t-att-title='_t("Minimize the conversation")'>Minimize</button>
+ <div class="btn-group">
+ <button type="button" class="btn btn-default btn-sm dropdown-toggle o_mail_chat_button_more" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ More <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li class="o_mail_chat_button_unsubscribe"><a href="#">Unsubscribe</a></li>
+ <li class="o_mail_chat_button_settings"><a href="#">Settings</a></li>
+ </ul>
+ </div>
+
+ </div>
+ </t>
+
+
+</templates>
diff --git a/addons/mail/static/src/xml/mail_chat_common.xml b/addons/mail/static/src/xml/mail_chat_common.xml
index eb479863b3e..745893c11e1 100644
--- a/addons/mail/static/src/xml/mail_chat_common.xml
+++ b/addons/mail/static/src/xml/mail_chat_common.xml
@@ -1,103 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
-<t t-name="im_chat.Conversation">
- <div class="oe_im_chatview_header">
- <span class="header_name"></span>
- <span class="nbr_messages"/>
- <span class="right_buttons">
- <!-- options -->
- <div t-if="widget.actions.length !== 0" class="btn-group button_option_group">
- <span class="dropdown-toggle" data-toggle="dropdown">
- <i class="fa fa-cogs"></i>
+
+ <t t-name="mail.chat.im.Conversation">
+ <div class="o_mail_chat_im_window openerp">
+ <div class="o_mail_chat_im_window_header">
+ <span class="o_mail_chat_im_window_header_name">
+ <t t-raw="widget.get('session').name"/>
+ </span>
+ <span class="nbr_messages o_mail_chat_im_window_header_counter"/>
+ <span class="o_mail_chat_im_window_header_buttons">
+ <div class="o_mail_chat_im_window_close">×</div>
</span>
- <ul class="dropdown-menu" role="menu">
- <t t-foreach="widget.actions" t-as="action">
- <li t-att-class="action.class">
- <i t-att-class="action.icon_class"></i> <t t-esc="action.label"/>
- </li>
- </t>
- </ul>
</div>
- <div class="oe_im_chatview_close">×</div>
- </span>
- </div>
- <div class="oe_im_chatview_content">
- <div class="oe_im_chatview_status"/>
- <div class="oe_im_chatview_content_bubbles"></div>
- </div>
- <div class="oe_im_chatview_footer">
- <input class="oe_im_chatview_input" t-att-placeholder="widget.inputPlaceholder" />
- </div>
-</t>
-<t t-name="im_chat.Conversation_content">
- <t t-foreach="_.keys(list)" t-as="date">
- <div class="oe_im_chatview_date_separator">
- <t t-esc="window.moment(date).format('LL')"/>
+ <div class="o_mail_chat_im_window_content">
+ <t t-call="mail.chat.im.Conversation.messages"/>
+ </div>
+ <div class="oe_im_chatview_footer o_mail_chat_im_window_footer">
+ <input class="o_mail_chat_im_window_input" t-att-placeholder="widget.options.placeholder"/>
+ </div>
</div>
- <t t-foreach="list[date]" t-as="bubble">
- <t t-if="bubble[0].type === 'message'">
- <t t-call="im_chat.Conversation_message_bubble">
- <t t-set="messages" t-value="bubble"/>
- </t>
- </t>
- <t t-if="bubble[0].type === 'meta'">
- <t t-call="im_chat.Conversation_technical_bubble">
- <t t-set="messages" t-value="bubble"/>
- </t>
- </t>
- </t>
</t>
-</t>
-<t t-name="im_chat.Conversation_message_bubble">
- <div class="oe_im_chatview_message_bubble">
- <div class="oe_im_chatview_clip">
- <img class="oe_im_chatview_avatar" t-att-src="_.last(messages).from_id[2]"/>
- </div>
- <div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
- <div class="oe_im_chatview_bubble_list">
- <t t-foreach="messages" t-as="m">
- <div class="oe_im_chatview_bubble_item"><t t-raw="m.message"/></div>
+
+ <t t-name="mail.chat.im.Conversation.messages">
+ <div class="o_mail_chat_im_messages">
+ <t t-set="current_day" t-value="0"/>
+ <t t-foreach="widget.get('messages')" t-as="message">
+ <div t-if="current_day != message.date.split(' ')[0]" class="row o_mail_chat_im_window_date_sperator">
+ <t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
+ <t t-set="current_day" t-value="message.date.split(' ')[0]"/>
+ </div>
+ <div class="o_mail_chat_im_message">
+ <div class="o_mail_chat_im_message_avatar">
+ <t t-if="message.author_avatar">
+ <img t-att-src="'data:image/png;base64,' + message.author_avatar" alt="avatar" class="img-responsive img-rounded"/>
+ </t>
+ <t t-if="!message.author_avatar">
+ <img src="/mail/static/src/img/smiley/avatar.jpg" alt="avatar" class="img-responsive img-rounded"/>
+ </t>
+ </div>
+ <div class="o_mail_chat_im_message_core">
+ <!-- author -->
+ <span class="text-muted">
+ <t t-if="message.author_id">
+ <strong><t t-esc="message.author_id[1]"/></strong> -
+ </t>
+ <t t-if="!message.author_id">
+ <strong><t t-esc="message.email_from"/></strong> -
+ </t>
+ <span class="small"><t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/></span>
+ </span>
+ <!-- body -->
+ <p class="o_mail_chat_im_message_body">
+ <t t-if="message.body">
+ <t t-raw="message.body"/> <!-- TODO JEM : tracking message and attachments ??!! -->
+ </t>
+ <t t-if="!message.body">
+ <t t-call="mail.chat.im.Conversation.tracking_message"/>
+ </t>
+ </p>
+ <!-- Attachments -->
+ <div t-attf-class="o_mail_chat_im_message_attachment">
+ <t t-foreach="message.attachment_ids" t-as="attachment">
+ <a t-att-href='attachment.url' target="_blank">
+ <i t-att-class="attachment.fa_class" t-att-title="attachment.name || attachment.filename"/>
+ </a>
+ </t>
+ </div>
+ </div>
+ </div>
</t>
</div>
- <div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
- </div>
-</t>
-<t t-name="im_chat.Conversation_technical_bubble">
- <div class="oe_im_chatview_technical_bubble">
- <div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
+ </t>
+
+ <t t-name="mail.chat.im.Conversation.tracking_message">
<div>
- <t t-foreach="messages" t-as="m">
- <div><t t-raw="m.message"/></div>
- </t>
+ <ul class="o_mail_message_tracking">
+ <t t-foreach='message.tracking_value_ids' t-as='value'>
+ <li>
+ <t t-esc="value.changed_field"/> :
+ <t t-if="value.old_value">
+ <span class="o_mail_message_tracking_value"> <t t-esc="value.old_value"/> </span>
+ <span> <i class="fa fa-long-arrow-right"></i> </span>
+ </t>
+ <span class="o_mail_message_tracking_value"> <t t-esc="value.new_value"/> </span>
+ </li>
+ </t>
+ </ul>
</div>
- <div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
- </div>
-</t>
+ </t>
-<t t-name="im_chat.UserWidget">
- <div class="oe_im_user ui-draggable">
- <span class="oe_im_user_clip">
- <img t-att-src="widget.get('image_url')" class="oe_im_user_avatar"/>
- </span>
- <span class="oe_im_user_name"><t t-esc="widget.get('name')"/></span>
- <img t-att-src="'/im_chat/static/src/img/green.png'" t-att-data-im-user-id="widget.get('id')" class="oe_im_user_online"/>
- </div>
-</t>
-<t t-name="im_chat.InstantMessaging">
- <div class="oe_im">
- <div class="oe_im_frame_header">
- <i class="fa fa-search oe_im_search_icon"/>
- <input class="oe_im_searchbox" placeholder="Search users..."/>
- </div>
- <div class="oe_im_users"></div>
- <div class="oe_im_content"></div>
- </div>
-</t>
-<t t-name="im_chat.ImTopButton">
- <li t-att-title='_t("Display Instant Messaging")'>
- <a href="#">
- <i id="oe_topbar_imbutton_icon" class="fa fa-comments"/>
- </a>
- </li>
-</t>
-</templates>
+</templates> \ No newline at end of file
diff --git a/addons/mail/static/src/xml/mail_chatter.xml b/addons/mail/static/src/xml/mail_chatter.xml
new file mode 100644
index 00000000000..2d2c150aaba
--- /dev/null
+++ b/addons/mail/static/src/xml/mail_chatter.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+
+ <t t-name="mail.chatter.ComposeMessage">
+ <div class="o_mail_chatter_compose_message">
+ <!-- Log a Note (subtypes)-->
+ <t t-if="widget.options.is_log">
+ <div class="form-group col-md-8">
+ <span class="text-muted">
+ Log an internal note which will not be sent to followers.<br/>
+ Note that users reading the document can read internal notes.
+ </span>
+ </div>
+ <!-- Internal subtypes -->
+ <div t-att-class="'form-group col-md-8 ' + (widget.options.internal_subtypes.length &lt;= 1 ? ' hide' : '')">
+ <label class="col-sm-2 control-label">Activity</label>
+ <div class="col-sm-4">
+ <select class="form-control o_mail_chatter_compose_message_subtype_select">
+ <t t-foreach="widget.options.internal_subtypes" t-as="subtype">
+ <option t-att-value="subtype['id']"><t t-raw="subtype['name']"/></option>
+ </t>
+ </select>
+ </div>
+ </div>
+ </t>
+ <!-- New Message (list of followers)-->
+ <t t-if="!widget.options.is_log">
+ <!-- Text for followers -->
+ <div class="form-group col-md-8 o_mail_chatter_compose_message_all_followers">
+ <span class="text-muted">
+ To:
+ <t t-if="widget.options.record_name">
+ Followers of
+ <t t-raw="'&quot;' + widget.options.record_name + '&quot;'"/>
+ </t>
+ <t t-if="!widget.options.record_name">
+ Followers of this document
+ </t>
+ </span>
+ <!-- List of followers -->
+ <div class="o_mail_chatter_compose_message_suggested_partners">
+ <t t-foreach='widget.suggested_partners' t-as='recipient'>
+ <div class="checkbox">
+ <label t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
+ <input type="checkbox"
+ t-att-checked="recipient.checked ? 'checked' : undefined"
+ t-att-data-fullname="recipient.full_name"/>
+ <t t-esc="recipient.name"/>
+ <t t-if="recipient.email_address">(<t t-esc="recipient.email_address"/>)</t>
+ <t t-if="!recipient.email_address">(no email address)</t>
+ </label>
+ </div>
+ </t>
+ </div>
+ </div>
+ </t>
+ <!-- Composer Message -->
+ <t t-call="mail.ComposeMessage"/>
+ </div>
+ </t>
+
+
+ <t t-name="mail.chatter.ChatterMailThread">
+ <div class="container-fluid o_mail_chatter o_mail_thread">
+ <!-- Compose buttons ('new message' and 'log a note') -->
+ <div class="row o_mail_chatter_compose_buttons">
+ <div class="col-md-7">
+ <div class="btn-group btn-group-sm">
+ <button type="button" class="btn btn-primary o_mail_chatter_button_new_message" title="Send a message">
+ New message
+ </button>
+ </div>
+ <t t-if="widget.options.display_log_button">
+ <span class="text-muted">or</span>
+ <div class="btn-group btn-group-sm">
+ <button class="btn btn-link btn-small o_mail_chatter_button_log_note" title="Log a note. Followers will not be notified.">
+ Log an internal note
+ </button>
+ </div>
+ </t>
+ </div>
+ </div>
+ <!-- wrap div for ChatterMailComposeMessage -->
+ <div class="row o_mail_chatter_compose_message_wrap" style="display:none;">
+
+ </div>
+
+ <!-- Message list -->
+ <div class="row">
+ <div class="col-md-10 col-md-offset-1 o_mail_chatter_messages">
+ <t t-call="mail.chatter.ChatterMailThread.messages"/>
+ </div>
+ </div>
+
+ <div t-if="widget.options.display_show_more_message" class="o_mail_chatter_show_more_message text-muted">
+ ----------- Show more message -----------
+ </div>
+ </div>
+ </t>
+
+ <t t-name="mail.chatter.ChatterMailThread.messages">
+ <t t-if="widget.get('messages').length">
+ <t t-call="mail.Thread.messages"/>
+ </t>
+ <t t-if="!widget.get('messages').length">
+ No Messages.
+ </t>
+ </t>
+
+ <!--
+ ComposeMessage.ButtonTopBar
+ render of the button on the user bar for open wizard compose message
+ -->
+ <t t-name="mail.ComposeMessageTopButton">
+ <li title='Compose new Message' class="oe_topbar_item oe_topbar_compose_full_email">
+ <a href="#">
+ <i class="fa fa-envelope-o fa-fw"></i>
+ </a>
+ </li>
+ </t>
+
+
+</templates>
diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml
index dd6d9883abd..b7548f861fd 100644
--- a/addons/mail/static/src/xml/mail_followers.xml
+++ b/addons/mail/static/src/xml/mail_followers.xml
@@ -2,76 +2,76 @@
<templates>
<!--
- followers main template
+ Mail Follower (root template)
Template used to display the followers, the actions and the subtypes in a record.
- -->
-<div t-name="mail_followers" class="o_timeline_followers">
- <div t-attf-class="btn-group-sm o_timeline_follower_title_box">
- <button t-attf-class="btn btn-default dropdown-toggle o_timeline_partner_list"
- data-toggle="dropdown" aria-expanded="false">
- <span class="o_timeline_follower_title">Followers</span>
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu o_timeline_follower_list" role="menu"></ul>
- </div>
- <div class="oe_actions">
- <div t-attf-class="btn-group-sm o_timeline_subtype_list">
- <button class="btn btn-default o_timeline_follower o_timeline_notfollow">
- <span class="o_timeline_follow">Follow</span>
- <span class="fa fa-times o_timeline_unfollow" aria-hidden="true"> Unfollow</span>
- <span class="fa fa-check o_timeline_following" aria-hidden="true"> Following</span>
- </button>
- <button t-attf-class="btn btn-default dropdown-toggle o_timeline_sbtp_list"
+-->
+<t t-name="mail.Followers">
+ <div class="o_timeline_followers">
+ <div t-attf-class="btn-group-sm o_timeline_follower_title_box">
+ <button t-attf-class="btn btn-default dropdown-toggle o_timeline_partner_list"
data-toggle="dropdown" aria-expanded="false">
+ <span class="o_timeline_follower_title">Followers</span>
<span class="caret"></span>
</button>
- <ul class="dropdown-menu oe_subtype" role="menu"></ul>
+ <ul class="dropdown-menu o_timeline_follower_list" role="menu"></ul>
+ </div>
+ <div class="oe_actions">
+ <div t-attf-class="btn-group-sm o_timeline_subtype_list">
+ <button class="btn btn-default o_timeline_follower o_timeline_notfollow">
+ <span class="o_timeline_follow">Follow</span>
+ <span class="fa fa-times o_timeline_unfollow" aria-hidden="true"> Unfollow</span>
+ <span class="fa fa-check o_timeline_following" aria-hidden="true"> Following</span>
+ </button>
+ <button t-attf-class="btn btn-default dropdown-toggle o_timeline_sbtp_list"
+ data-toggle="dropdown" aria-expanded="false">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu oe_subtype" role="menu"></ul>
+ </div>
</div>
-
- <!--<t t-if="widget.comment">
- <h5 class="oe_comment"><t t-raw="widget.comment"/></h5>
- </t>-->
</div>
-</div>
+</t>
<!--
- followers.partner template
+ Mail Followers Partner template
Template used to display a partner following the record
- -->
-<li t-name="mail_followers_partner" class='oe_partner'>
- <span> <img class="oe_mail_thumbnail oe_mail_frame" t-att-src="record.avatar_url"/> </span>
- <span class="oe_partner_name">
- <a t-attf-href="#model=res.partner&amp;id=#{record.id}"
- t-att-title="record.name"
- t-att-data-res-model="record.res_model"
- t-att-data-res-id="record.res_id"
- t-att-data-id="record.id">
- <t t-esc="record.name"/></a>
- </span>
- <i t-if="record.is_editable and (widget.records_length &gt; 1)"
- class="oe_edit_subtype fa fa-pencil oe_hidden" title="Edit subscription"
- t-att-data-id="record.id"/>
- <i t-if="widget.view_is_editable" class="oe_remove_follower fa fa-remove"
- title="Remove this follower"/>
-</li>
+-->
+<t t-name="mail.Followers.partner">
+ <li class='oe_partner'>
+ <span> <img class="oe_mail_thumbnail oe_mail_frame" t-att-src="record.avatar_url"/> </span>
+ <span class="oe_partner_name">
+ <a t-attf-href="#model=res.partner&amp;id=#{record.id}"
+ t-att-title="record.name"
+ t-att-data-res-model="record.res_model"
+ t-att-data-res-id="record.res_id"
+ t-att-data-id="record.id">
+ <t t-esc="record.name"/></a>
+ </span>
+ <i t-if="record.is_editable and (widget.records_length &gt; 1)"
+ class="oe_edit_subtype fa fa-pencil oe_hidden" title="Edit subscription"
+ t-att-data-id="record.id"/>
+ <i t-if="widget.view_is_editable" class="oe_remove_follower fa fa-remove"
+ title="Remove this follower"/>
+ </li>
+</t>
-<t t-name="mail_followers_add_more">
- <li t-if="widget.view_is_editable" class="oe_invite">
+<t t-name="mail.Followers.add_more">
+ <li t-if="widget.view_is_editable" class="oe_invite">
<a href="#"> Add followers </a>
</li>
<li t-if="widget.view_is_editable and (widget.followers.length > 0)" class="divider"></li>
</t>
<!--
- followers.subtype template
+ Mail Followers Subtype template
Template used to display message subtypes of a follower subscription
-->
-<t t-name="mail_followers_subtype">
+<t t-name="mail.Followers.subtype">
<li class="oe_subtype">
- <input type="checkbox" t-att-checked="record.followed"
+ <input type="checkbox" t-att-checked="record.followed"
t-att-id="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')"
t-att-data-id="record.id" t-att-name="record.name" class="o_timeline_msg_subtype_check"/>
-
+
<label t-att-for="'input_mail_followers_subtype_'+record.id+(dialog ? '_in_dialog': '')">
<t t-raw="record.name"/>
</label>
diff --git a/addons/mail/static/src/xml/mail_thread.xml b/addons/mail/static/src/xml/mail_thread.xml
new file mode 100644
index 00000000000..10baacc0577
--- /dev/null
+++ b/addons/mail/static/src/xml/mail_thread.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates>
+
+ <!--
+ Mail Composer Message : input with 2 buttons (emoji and attachment)
+ -->
+ <t t-name="mail.ComposeMessage">
+ <div class="form-group col-md-12 o_mail_compose_message">
+ <!-- input text : depending on the display_mode option -->
+ <div t-att-class="widget.options.display_mode === 'chat' ? 'input-group o_mail_compose_message_mode_chat' : 'o_mail_compose_message_mode_mail'">
+ <t t-if="widget.options.display_mode === 'chat'">
+ <input type="text" class="form-control o_mail_compose_message_input" placeholder="Write something" name="message" id="send_message"/>
+ </t>
+ <t t-if="widget.options.display_mode === 'mail'">
+ <textarea class="form-control o_mail_compose_message_input" placeholder="Write something" name="message" id="send_message"/>
+ </t>
+ <span class="input-group-btn o_mail_compose_message_button_group">
+ <button t-if="widget.options.display_mode === 'mail'" class="btn btn-primary o_mail_compose_message_button_send" type="button">
+ Send
+ </button>
+ <button class="btn btn-default o_mail_compose_message_button_emoji" type="button" data-toggle="popover">
+ <i class="fa fa-smile-o"/>
+ </button>
+ <button class="btn btn-default o_mail_compose_message_button_attachment" type="button" data-toggle="popover">
+ <i class="fa fa-paperclip"/>
+ </button>
+ </span>
+ </div>
+
+ <span class="o_mail_attachment_file hide">
+ <t t-call="HiddenInputFile">
+ <t t-set="fileupload_id" t-value="widget.fileupload_id"/>
+ <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
+ <input type="hidden" name="model" value="mail.compose.message"/>
+ <input type="hidden" name="id" value="0"/>
+ <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
+ </t>
+ </span>
+ <span class="o_mail_compose_message_emoji"></span>
+
+ <div class="o_mail_compose_message_attachment_list"></div>
+ </div>
+ </t>
+
+ <t t-name="mail.ComposeMessage.emoji">
+ <div class="o_mail_compose_message_emoji_content">
+ <t t-foreach="widget.emoji_list" t-as="emoji">
+ <span t-att-data-emoji="emoji.source" class="o_mail_compose_message_emoji_img" t-att-title="emoji.description">
+ <t t-raw="emoji.substitution"/>
+ </span>
+ </t>
+ </div>
+ </t>
+
+ <t t-name="mail.ComposeMessage.attachments">
+ <div class="o_mail_chat_im_attachment_files">
+ <t t-foreach="widget.get('attachment_ids')" t-as='attachment'>
+ <t t-if="attachment.file_type_icon !== 'webimage'">
+ <div t-attf-class="o_mail_attachment #{attachment.upload ? 'o_mail_uploading' : ''}">
+ <a t-att-href='attachment.url' target="_blank">
+ <img t-att-src="'/mail/static/src/img/mimetypes/' + (attachment.file_type_icon || 'unknown') + '.png'"></img>
+ <div class='o_mail_attachment_name'><t t-raw="attachment.name"/></div>
+ </a>
+ <div class="o_mail_attachment_delete">
+ <i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
+ </div>
+ <div class="o_mail_attachment_progress_bar">
+ Uploading
+ </div>
+ </div>
+ </t>
+ <t t-if="attachment.file_type_icon === 'webimage'">
+ <div t-attf-class="o_mail_attachment o_mail_attachment_preview #{attachment.upload ? 'o_mail_uploading' : ''}">
+ <a t-att-href='attachment.url' target="_blank">
+ <img t-att-src="_.str.sprintf('/web/binary/image?model=ir.attachment&amp;field=datas&amp;id=%s', attachment.id)"/>
+ <div class='o_mail_attachment_name'><t t-raw='attachment.name'/></div>
+ </a>
+ <div class="o_mail_attachment_delete">
+ <i class='fa fa-times-circle' title="Delete this attachment" t-att-data-id="attachment.id"/>
+ </div>
+ <div class="o_mail_attachment_progress_bar">
+ Uploading
+ </div>
+ </div>
+ </t>
+ </t>
+ </div>
+ </t>
+
+
+ <!--
+ Message Thread
+ -->
+ <t t-name="mail.Thread.messages">
+ <t t-set="current_day" t-value="0"/>
+ <t t-foreach="widget.get('messages')" t-as="message">
+ <div t-if="current_day != message.date.split(' ')[0]" class="row o_mail_thread_date_separator">
+ <div class="o_mail_thread_date_wrapper">
+ <div class="o_mail_thread_date_separator_title">
+ <t t-raw="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LL')"/>
+ </div>
+ </div>
+ <t t-set="current_day" t-value="message.date.split(' ')[0]"/>
+ </div>
+ <div class="row o_mail_thread_message" t-att-data-message-id="message.id">
+ <div class="col-md-1 col-sm-1 o_mail_thread_message_avatar">
+ <t t-if="message.author_avatar">
+ <img t-att-src="'data:image/png;base64,' + message.author_avatar" alt="avatar" class="o_mail_thread_avatar img-rounded"/>
+ </t>
+ <t t-if="!message.author_avatar">
+ <img t-if="message.message_type == 'email' &amp;&amp; (!message.author_id || !message.author_id[0]) &amp;&amp; message.email_from" src="/mail/static/src/img/email_icon.png" alt="avatar" class="o_mail_thread_avatar img-rounded"/>
+ <img t-if="message.message_type == 'email' &amp;&amp; (message.author_id &amp;&amp; message.author_id[0]) &amp;&amp; !message.email_from" t-att-src="'/web/binary/image?model=res.partner&amp;field=image_small&amp;id=' + message.author_id[0]" class="o_mail_thread_avatar img-rounded"/>
+ <img t-if="message.message_type !== 'email' &amp;&amp; (message.author_id &amp;&amp; message.author_id[0])" t-att-src="'/web/binary/image?model=res.partner&amp;field=image_small&amp;id=' + message.author_id[0]" class="o_mail_thread_avatar img-rounded"/>
+ <img t-if="message.message_type !== 'email' &amp;&amp; (!message.author_id || !message.author_id[0])" src="/mail/static/src/img/smiley/avatar.jpg" class="o_mail_thread_avatar img-rounded"/>
+ </t>
+ </div>
+ <div t-att-class="'col-md-10 col-sm-10 o_mail_thread_message_core ' + 'o_mail_thread_message_type_' + message.message_type + ' ' + (message.is_note ? 'o_mail_thread_message_subtype_note' : '')">
+ <!-- actions -->
+ <span class="pull-right o_mail_thread_message_actions">
+ <i t-if="message.is_needaction &amp;&amp; widget.options.display_needaction_button" class="fa fa-check o_mail_thread_message_needaction" t-att-data-message-id="message.id" t-att-title="_t('Mark as Done')"/>
+ <i t-att-class="'fa fa-star-o fa-lg o_mail_thread_message_star ' + (message.is_starred ? 'o_mail_message_starred' : '')" t-att-data-message-id="message.id" t-att-title="_t('Mark as Todo')"/>
+ </span>
+
+ <p class="text-muted o_mail_thread_message_info">
+ <t t-if="message.is_note">
+ Note by
+ </t>
+ <!-- author : partner or email_from -->
+ <t t-if="message.author_id">
+ <strong class="o_mail_redirect" data-oe-model="res.partner" t-att-data-oe-id="message.author_id[0]"><t t-esc="message.author_id[1]"/></strong> -
+ </t>
+ <t t-if="!message.author_id">
+ <strong><t t-esc="message.email_from"/></strong> -
+ </t>
+ <small t-att-title="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('LLL')">
+ <t t-esc="moment(message.date, 'YYYY-MM-DD hh:mm:ss').format('hh:mm')"/>
+ </small>
+ <t t-if="message.model &amp;&amp; (message.model != 'mail.channel') &amp;&amp; widget.options.display_document_link">
+ on <a class="o_mail_redirect" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" href="#"><t t-esc="message.record_name"/></a>
+ </t>
+ </p>
+ <!-- tracking message-->
+ <t t-if="message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0">
+ <t t-call="mail.Thread.tracking_message">
+ <t t-set="message" t-value="message"/>
+ </t>
+ </t>
+ <!-- normal message -->
+ <t t-if="!(message.tracking_value_ids &amp;&amp; message.tracking_value_ids.length &gt; 0)">
+ <t t-raw="message.body"/>
+ </t>
+ <!-- Attachments -->
+ <t t-foreach="message.attachment_ids" t-as="attachment">
+ <div t-attf-class="o_mail_attachment">
+ <a t-att-href='attachment.url' target="_blank">
+ <img t-att-src="'/mail/static/src/img/mimetypes/' + (attachment.file_type_icon || 'unknown') + '.png'"></img>
+ <div class='o_mail_attachment_name'><t t-raw='attachment.name'/></div>
+ </a>
+ </div>
+ </t>
+ </div>
+
+ <div class="col-md-1">
+ </div>
+ </div>
+ </t>
+ </t>
+
+ <t t-name="mail.Thread.tracking_message">
+ <div>
+ <ul class="o_mail_thread_message_tracking">
+ <t t-foreach='message.tracking_value_ids' t-as='value'>
+ <li>
+ <t t-esc="value.changed_field"/> :
+ <t t-if="value.old_value">
+ <span class="o_mail_message_tracking_value"> <t t-esc="value.old_value"/> </span>
+ <span> <i class="fa fa-long-arrow-right"></i> </span>
+ </t>
+ <span class="o_mail_message_tracking_value"> <t t-esc="value.new_value"/> </span>
+ </li>
+ </t>
+ </ul>
+ </div>
+ </t>
+
+</templates>