diff options
Diffstat (limited to 'addons/mail/static/src')
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') > 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 > 0 ? 'o_mail_chat_channel_unread' : ' ')" t-att-data-channel-id="channel.id" role="presentation"> + <a t-att-href="_.str.sprintf('#active_id=%s&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 > 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 <= 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="'"' + widget.options.record_name + '"'"/> + </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&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 > 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&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 > 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&field=datas&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' && (!message.author_id || !message.author_id[0]) && 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' && (message.author_id && message.author_id[0]) && !message.email_from" t-att-src="'/web/binary/image?model=res.partner&field=image_small&id=' + message.author_id[0]" class="o_mail_thread_avatar img-rounded"/> + <img t-if="message.message_type !== 'email' && (message.author_id && message.author_id[0])" t-att-src="'/web/binary/image?model=res.partner&field=image_small&id=' + message.author_id[0]" class="o_mail_thread_avatar img-rounded"/> + <img t-if="message.message_type !== 'email' && (!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 && 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 && (message.model != 'mail.channel') && 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 && message.tracking_value_ids.length > 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 && message.tracking_value_ids.length > 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> |
