summaryrefslogtreecommitdiff
path: root/addons/mail/static/src
diff options
context:
space:
mode:
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>