From 3fea5b2136627c1d7e83b2a5e111952fe3eb10a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Mon, 15 Jun 2020 18:12:21 +0000 Subject: [PATCH] [REF] mail, *: refactor messaging with OWL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit is a significant rewriting of client-side discuss, chatter, chat window, and messaging menu using OWL. The behavior should be broadly the same, with some slight functional changes here and there. From a technical standpoint, the code of messaging is mainly organized in 2 main groups of modules: - models, which are logical entities that depict the client-side state of messaging as a whole. - components, which are in charge of displaying information from models. This refactoring also introduces new JS guidelines regarding folder structure (/static) and naming rules for JS modules. Community PR: https://github.com/odoo/odoo/pull/39023 Enterprise PR: https://github.com/odoo/enterprise/pull/6249 Task-1914207 This PR is a collaborative work by Alexandre, Julien, Sébastien and Xavier, with the precious help of Lucas to speed it up towards the end. closes odoo/odoo#39023 Related: odoo/enterprise#6249 Signed-off-by: Alexandre Kühn (aku) Co-authored-by: Alexandre Kühn Co-authored-by: Julien Giannone Co-authored-by: Lucas Perais Co-authored-by: Sébastien Theys Co-authored-by: Xavier Dubuc --- .../views/account_bank_statement_views.xml | 4 +- .../account/views/account_journal_views.xml | 6 +- addons/account/views/account_move_views.xml | 6 +- addons/account/views/account_payment_view.xml | 6 +- .../analytic/views/analytic_account_views.xml | 4 +- .../tests/systray_activity_menu_tests.js | 13 +- addons/calendar/views/calendar_views.xml | 4 +- addons/crm/views/crm_lead_views.xml | 6 +- addons/event/views/event_views.xml | 12 +- .../fleet/views/fleet_vehicle_cost_views.xml | 12 +- addons/fleet/views/fleet_vehicle_views.xml | 6 +- addons/gamification/views/challenge.xml | 4 +- addons/hr/static/src/js/chat.js | 19 +- .../tests/many2one_avatar_employee_tests.js | 34 +- addons/hr/views/hr_department_views.xml | 4 +- addons/hr/views/hr_employee_views.xml | 6 +- addons/hr/views/hr_job_views.xml | 2 +- .../hr_contract/views/hr_contract_views.xml | 6 +- addons/hr_expense/views/hr_expense_views.xml | 12 +- addons/hr_holidays/__manifest__.py | 4 + .../hr_holidays/static/src/bugfix/bugfix.js | 10 + .../hr_holidays/static/src/bugfix/bugfix.scss | 6 + .../hr_holidays/static/src/bugfix/bugfix.xml | 11 + .../static/src/bugfix/bugfix_tests.js | 18 + .../partner_im_status_icon.scss | 12 + .../partner_im_status_icon.xml | 13 + .../components/thread_icon/thread_icon.scss | 11 + .../components/thread_icon/thread_icon.xml | 13 + .../components/thread_viewer/thread_viewer.js | 58 + .../thread_viewer/thread_viewer.scss | 8 + .../thread_viewer/thread_viewer.xml | 10 + addons/hr_holidays/static/src/xml/discuss.xml | 9 - .../views/hr_leave_allocation_views.xml | 6 +- .../hr_holidays/views/hr_leave_template.xml | 7 + addons/hr_holidays/views/hr_leave_views.xml | 6 +- .../views/hr_recruitment_views.xml | 6 +- addons/im_livechat/__manifest__.py | 6 +- addons/im_livechat/controllers/main.py | 7 +- addons/im_livechat/models/mail_channel.py | 22 +- .../im_livechat/static/src/bugfix/bugfix.js | 10 + .../im_livechat/static/src/bugfix/bugfix.scss | 6 + .../im_livechat/static/src/bugfix/bugfix.xml | 11 + .../static/src/bugfix/bugfix_tests.js | 18 + .../static/src/components/discuss/discuss.js | 29 + .../src/components/discuss/discuss_tests.js | 93 + .../discuss_sidebar/discuss_sidebar.js | 70 + .../discuss_sidebar/discuss_sidebar.xml | 25 + .../discuss_sidebar_item.js | 26 + .../notification_list/notification_list.js | 36 + .../components/thread_icon/thread_icon.xml | 19 + .../thread_icon/thread_icon_tests.js | 128 + .../thread_needaction_preview.js | 28 + .../thread_preview/thread_preview.js | 28 + .../thread_textual_typing_status_tests.js | 123 + .../im_livechat/static/src/js/im_livechat.js | 576 -- .../static/src/js/im_livechat_backend.js | 32 - .../static/src/js/models/website_livechat.js | 186 - .../src/js/models/website_livechat_message.js | 66 - .../static/src/js/website_livechat_window.js | 105 - .../static/src/legacy/public_livechat.js | 3861 ++++++++++++ .../static/src/legacy/public_livechat.scss | 634 ++ .../static/src/legacy/public_livechat.xml | 658 ++ .../messaging_initializer.js | 29 + .../messaging_notification_handler.js | 38 + .../static/src/models/thread/thread.js | 61 + .../static/src/scss/im_livechat.scss | 90 - .../static/src/xml/im_livechat.xml | 42 - .../static/src/xml/im_livechat_backend.xml | 26 - .../static/tests/discuss_livechat_tests.js | 74 - .../tests/website_livechat_window_tests.js | 114 - .../tests/test_get_mail_channel.py | 4 - .../views/im_livechat_channel_templates.xml | 50 +- addons/lunch/views/lunch_supplier_views.xml | 6 +- addons/mail/__manifest__.py | 65 +- addons/mail/controllers/main.py | 2 + addons/mail/data/mail_data.xml | 2 +- addons/mail/models/mail_activity.py | 2 +- addons/mail/models/mail_channel.py | 10 +- addons/mail/models/mail_message.py | 24 +- addons/mail/models/mail_notification.py | 17 +- addons/mail/models/mail_thread.py | 15 - addons/mail/models/res_partner.py | 8 + addons/mail/static/src/bugfix/bugfix.js | 10 + addons/mail/static/src/bugfix/bugfix.scss | 6 + addons/mail/static/src/bugfix/bugfix.xml | 11 + addons/mail/static/src/bugfix/bugfix_tests.js | 18 + .../use_drag_visible_dropzone.js | 88 + .../src/component_hooks/use_refs/use_refs.js | 21 + .../component_hooks/use_store/use_store.js | 126 + .../use_store/use_store_tests.js | 190 + .../src/components/activity/activity.js | 177 + .../src/components/activity/activity.scss | 174 + .../src/components/activity/activity.xml | 149 + .../src/components/activity/activity_tests.js | 981 +++ .../components/activity_box/activity_box.js | 60 + .../components/activity_box/activity_box.scss | 41 + .../components/activity_box/activity_box.xml | 45 + .../activity_mark_done_popover.js | 83 + .../activity_mark_done_popover.scss | 22 + .../activity_mark_done_popover.xml | 23 + .../activity_mark_done_popover_tests.js | 282 + .../src/components/attachment/attachment.js | 162 + .../src/components/attachment/attachment.scss | 196 + .../src/components/attachment/attachment.xml | 109 + .../components/attachment/attachment_tests.js | 545 ++ .../attachment_box/attachment_box.js | 118 + .../attachment_box/attachment_box.scss | 46 + .../attachment_box/attachment_box.xml | 50 + .../attachment_box/attachment_box_tests.js | 228 + .../attachment_list/attachment_list.js | 54 + .../attachment_list/attachment_list.scss | 13 + .../attachment_list/attachment_list.xml | 22 + .../attachment_viewer/attachment_viewer.js | 593 ++ .../attachment_viewer/attachment_viewer.scss | 198 + .../attachment_viewer/attachment_viewer.xml | 93 + .../autocomplete_input/autocomplete_input.js | 161 + .../autocomplete_input/autocomplete_input.xml | 8 + .../src/components/chat_window/chat_window.js | 352 ++ .../components/chat_window/chat_window.scss | 101 + .../components/chat_window/chat_window.xml | 56 + .../chat_window_header/chat_window_header.js | 107 + .../chat_window_header.scss | 95 + .../chat_window_header/chat_window_header.xml | 53 + .../chat_window_hidden_menu.js | 142 + .../chat_window_hidden_menu.scss | 90 + .../chat_window_hidden_menu.xml | 31 + .../chat_window_manager.js | 37 + .../chat_window_manager.scss | 16 + .../chat_window_manager.xml | 21 + .../chat_window_manager_tests.js | 1243 ++++ .../static/src/components/chatter/chatter.js | 103 + .../src/components/chatter/chatter.scss | 37 + .../static/src/components/chatter/chatter.xml | 53 + .../src/components/chatter/chatter_tests.js | 337 + .../chatter_container/chatter_container.js | 80 + .../chatter_container/chatter_container.scss | 13 + .../chatter_container/chatter_container.xml | 15 + .../chatter_topbar/chatter_topbar.js | 128 + .../chatter_topbar/chatter_topbar.scss | 100 + .../chatter_topbar/chatter_topbar.xml | 72 + .../chatter_topbar/chatter_topbar_tests.js | 590 ++ .../src/components/composer/composer.js | 393 ++ .../src/components/composer/composer.scss | 278 + .../src/components/composer/composer.xml | 154 + .../src/components/composer/composer_tests.js | 866 +++ .../composer_text_input.js | 334 + .../composer_text_input.scss | 25 + .../composer_text_input.xml | 44 + .../static/src/components/dialog/dialog.js | 105 + .../static/src/components/dialog/dialog.scss | 23 + .../static/src/components/dialog/dialog.xml | 22 + .../dialog_manager/dialog_manager.js | 58 + .../dialog_manager/dialog_manager.xml | 15 + .../static/src/components/discuss/discuss.js | 277 + .../src/components/discuss/discuss.scss | 114 + .../static/src/components/discuss/discuss.xml | 106 + .../src/components/discuss/discuss_tests.js | 5523 +++++++++++++++++ .../discuss_mobile_mailbox_selection.js | 89 + .../discuss_mobile_mailbox_selection.scss | 26 + .../discuss_mobile_mailbox_selection.xml | 20 + .../discuss_sidebar/discuss_sidebar.js | 307 + .../discuss_sidebar/discuss_sidebar.scss | 110 + .../discuss_sidebar/discuss_sidebar.xml | 80 + .../discuss_sidebar_item.js | 203 + .../discuss_sidebar_item.scss | 107 + .../discuss_sidebar_item.xml | 60 + .../src/components/drop_zone/drop_zone.js | 136 + .../src/components/drop_zone/drop_zone.scss | 29 + .../src/components/drop_zone/drop_zone.xml | 12 + .../components/editable_text/editable_text.js | 82 + .../editable_text/editable_text.xml | 8 + .../emojis_popover/emojis_popover.js | 42 + .../emojis_popover/emojis_popover.scss | 21 + .../emojis_popover/emojis_popover.xml | 14 + .../components/file_uploader/file_uploader.js | 206 + .../file_uploader/file_uploader.scss | 3 + .../file_uploader/file_uploader.xml | 10 + .../file_uploader/file_uploader_tests.js | 104 + .../components/follow_button/follow_button.js | 91 + .../follow_button/follow_button.scss | 19 + .../follow_button/follow_button.xml | 24 + .../follow_button/follow_button_tests.js | 331 + .../src/components/follower/follower.js | 81 + .../src/components/follower/follower.scss | 55 + .../src/components/follower/follower.xml | 23 + .../src/components/follower/follower_tests.js | 354 ++ .../follower_list_menu/follower_list_menu.js | 152 + .../follower_list_menu.scss | 19 + .../follower_list_menu/follower_list_menu.xml | 36 + .../follower_list_menu_tests.js | 465 ++ .../follower_subtype/follower_subtype.js | 69 + .../follower_subtype/follower_subtype.scss | 27 + .../follower_subtype/follower_subtype.xml | 13 + .../follower_subtype_tests.js | 241 + .../follower_subtype_list.js | 87 + .../follower_subtype_list.scss | 8 + .../follower_subtype_list.xml | 38 + .../components/mail_template/mail_template.js | 79 + .../mail_template/mail_template.scss | 27 + .../mail_template/mail_template.xml | 29 + .../static/src/components/message/message.js | 621 ++ .../src/components/message/message.scss | 354 ++ .../static/src/components/message/message.xml | 220 + .../src/components/message/message_tests.js | 692 +++ .../message_author_prefix.js | 65 + .../message_author_prefix.scss | 11 + .../message_author_prefix.xml | 17 + .../components/message_list/message_list.js | 531 ++ .../components/message_list/message_list.scss | 135 + .../components/message_list/message_list.xml | 107 + .../message_seen_indicator.js | 130 + .../message_seen_indicator.scss | 39 + .../message_seen_indicator.xml | 16 + .../message_seen_indicator_tests.js | 293 + .../messaging_menu/messaging_menu.js | 266 + .../messaging_menu/messaging_menu.scss | 133 + .../messaging_menu/messaging_menu.xml | 89 + .../messaging_menu/messaging_menu_tests.js | 849 +++ .../mobile_messaging_navbar.js | 50 + .../mobile_messaging_navbar.scss | 43 + .../mobile_messaging_navbar.xml | 17 + .../moderation_ban_dialog.js | 79 + .../moderation_ban_dialog.xml | 26 + .../moderation_discard_dialog.js | 94 + .../moderation_discard_dialog.xml | 16 + .../moderation_reject_dialog.js | 89 + .../moderation_reject_dialog.xml | 16 + .../notification_group/notification_group.js | 85 + .../notification_group.scss | 89 + .../notification_group/notification_group.xml | 39 + .../notification_list/notification_list.js | 216 + .../notification_list/notification_list.scss | 37 + .../notification_list/notification_list.xml | 41 + .../notification_list_item.scss | 174 + ...ification_list_notification_group_tests.js | 563 ++ .../notification_list_tests.js | 210 + .../notification_popover.js | 89 + .../notification_popover.scss | 7 + .../notification_popover.xml | 17 + .../partner_im_status_icon.js | 52 + .../partner_im_status_icon.scss | 53 + .../partner_im_status_icon.xml | 36 + .../partner_im_status_icon_tests.js | 152 + .../partner_mention_suggestion.js | 79 + .../partner_mention_suggestion.scss | 26 + .../partner_mention_suggestion.xml | 19 + .../partner_mention_suggestion_tests.js | 141 + .../src/components/thread_icon/thread_icon.js | 54 + .../components/thread_icon/thread_icon.scss | 26 + .../components/thread_icon/thread_icon.xml | 58 + .../thread_icon/thread_icon_tests.js | 146 + .../thread_needaction_preview.js | 131 + .../thread_needaction_preview.scss | 104 + .../thread_needaction_preview.xml | 58 + .../thread_preview/thread_preview.js | 124 + .../thread_preview/thread_preview.scss | 113 + .../thread_preview/thread_preview.xml | 63 + .../thread_textual_typing_status.js | 49 + .../thread_textual_typing_status.scss | 12 + .../thread_textual_typing_status.xml | 14 + .../thread_textual_typing_status_tests.js | 480 ++ .../thread_typing_icon/thread_typing_icon.js | 32 + .../thread_typing_icon.scss | 108 + .../thread_typing_icon/thread_typing_icon.xml | 29 + .../components/thread_viewer/thread_viewer.js | 199 + .../thread_viewer/thread_viewer.scss | 38 + .../thread_viewer/thread_viewer.xml | 44 + .../thread_viewer/thread_viewer_tests.js | 364 ++ addons/mail/static/src/env/env.js | 104 + addons/mail/static/src/env/test_env.js | 152 + addons/mail/static/src/js/activity.js | 3 +- addons/mail/static/src/js/attachment_box.js | 141 - addons/mail/static/src/js/basic_model.js | 33 - addons/mail/static/src/js/basic_view.js | 48 +- addons/mail/static/src/js/chatter.js | 660 -- .../static/src/js/composers/basic_composer.js | 864 --- .../src/js/composers/chatter_composer.js | 331 - .../src/js/composers/extended_composer.js | 138 - .../src/js/composers/mention_manager.js | 439 -- addons/mail/static/src/js/discuss.js | 1642 ----- addons/mail/static/src/js/document_viewer.js | 8 +- addons/mail/static/src/js/followers.js | 461 -- addons/mail/static/src/js/form_controller.js | 47 - addons/mail/static/src/js/form_renderer.js | 77 - addons/mail/static/src/js/form_view.js | 26 - .../static/src/js/many2one_avatar_user.js | 7 +- .../mail/static/src/js/models/mail_failure.js | 127 - .../js/models/messages/abstract_message.js | 462 -- .../static/src/js/models/messages/message.js | 806 --- .../src/js/models/threads/abstract_thread.js | 231 - .../static/src/js/models/threads/channel.js | 474 -- .../threads/create_mode_document_thread.js | 75 - .../static/src/js/models/threads/dm_chat.js | 109 - .../src/js/models/threads/document_thread.js | 364 -- .../static/src/js/models/threads/livechat.js | 95 - .../static/src/js/models/threads/mailbox.js | 224 - .../threads/mixins/channel_seen_mixin.js | 229 - .../threads/mixins/thread_typing_mixin.js | 360 -- .../js/models/threads/multi_user_channel.js | 73 - .../js/models/threads/searchable_thread.js | 244 - .../static/src/js/models/threads/thread.js | 383 -- .../src/js/models/threads/two_user_channel.js | 32 - .../js/models/utils/cc_throttle_function.js | 181 - .../mail/static/src/js/models/utils/timer.js | 69 - .../mail/static/src/js/models/utils/timers.js | 82 - .../services/mail_document_thread_manager.js | 260 - .../static/src/js/services/mail_manager.js | 1356 ---- .../js/services/mail_notification_manager.js | 597 -- .../static/src/js/services/mail_service.js | 17 - .../src/js/services/mail_status_manager.js | 232 - .../src/js/services/mail_window_manager.js | 780 --- .../src/js/systray/systray_activity_menu.js | 4 +- .../src/js/systray/systray_messaging_menu.js | 394 -- addons/mail/static/src/js/thread_field.js | 265 - addons/mail/static/src/js/thread_widget.js | 686 -- .../thread_windows/abstract_thread_window.js | 519 -- .../src/js/thread_windows/thread_window.js | 403 -- .../mail/static/src/js/tools/debug_manager.js | 1 - addons/mail/static/src/js/tours/mail.js | 20 +- addons/mail/static/src/js/utils.js | 38 +- addons/mail/static/src/model/model_core.js | 125 + addons/mail/static/src/model/model_errors.js | 22 + addons/mail/static/src/model/model_field.js | 988 +++ addons/mail/static/src/model/model_manager.js | 947 +++ .../static/src/models/activity/activity.js | 289 + .../src/models/activity_type/activity_type.js | 26 + .../src/models/attachment/attachment.js | 342 + .../src/models/attachment/attachment_tests.js | 156 + .../attachment_viewer/attachment_viewer.js | 44 + .../src/models/chat_window/chat_window.js | 322 + .../chat_window_manager.js | 461 ++ .../mail/static/src/models/chatter/chatter.js | 326 + .../static/src/models/composer/composer.js | 593 ++ .../mail/static/src/models/country/country.js | 23 + .../mail/static/src/models/device/device.js | 71 + .../mail/static/src/models/dialog/dialog.js | 52 + .../models/dialog_manager/dialog_manager.js | 99 + .../mail/static/src/models/discuss/discuss.js | 402 ++ .../static/src/models/follower/follower.js | 265 + .../follower_subtype/follower_subtype.js | 71 + .../follower_subtype_list.js | 22 + .../mail/static/src/models/locale/locale.js | 38 + .../src/models/mail_template/mail_template.js | 76 + .../mail/static/src/models/message/message.js | 587 ++ .../src/models/message/message_tests.js | 132 + .../message_seen_indicator.js | 286 + .../static/src/models/messaging/messaging.js | 216 + .../src/models/messaging/messaging_tests.js | 66 + .../messaging_initializer.js | 292 + .../models/messaging_menu/messaging_menu.js | 131 + .../messaging_notification_handler.js | 744 +++ addons/mail/static/src/models/model/model.js | 245 + .../src/models/notification/notification.js | 69 + .../notification_group/notification_group.js | 115 + .../notification_group_manager.js | 76 + .../mail/static/src/models/partner/partner.js | 273 + .../mail/static/src/models/thread/thread.js | 1370 ++++ .../static/src/models/thread/thread_tests.js | 162 + .../src/models/thread_cache/thread_cache.js | 263 + .../thread_partner_seen_info.js | 28 + .../src/models/thread_viewer/thread_viewer.js | 270 + addons/mail/static/src/models/user/user.js | 78 + .../src/scss/abstract_thread_window.scss | 194 - .../mail/static/src/scss/attachment_box.scss | 34 - addons/mail/static/src/scss/chatter.scss | 35 - addons/mail/static/src/scss/composer.scss | 342 - addons/mail/static/src/scss/discuss.scss | 308 - addons/mail/static/src/scss/followers.scss | 92 - .../mail/static/src/scss/out_of_office.scss | 8 - addons/mail/static/src/scss/thread.scss | 167 +- .../mail/static/src/scss/thread_window.scss | 63 - .../chat_window_service.js | 104 + .../services/dialog_service/dialog_service.js | 101 + .../static/src/utils/deferred/deferred.js | 21 + addons/mail/static/src/utils/test_utils.js | 860 +++ .../static/src/utils/throttle/throttle.js | 382 ++ .../src/utils/throttle/throttle_tests.js | 410 ++ addons/mail/static/src/utils/timer/timer.js | 165 + .../static/src/utils/timer/timer_tests.js | 431 ++ addons/mail/static/src/utils/utils.js | 127 + addons/mail/static/src/variables.scss | 14 + addons/mail/static/src/widgets/common.xml | 25 + .../static/src/widgets/discuss/discuss.js | 383 ++ .../static/src/widgets/discuss/discuss.scss | 36 + .../static/src/widgets/discuss/discuss.xml | 27 + .../discuss_invite_partner_dialog.js | 125 + .../discuss_invite_partner_dialog.xml | 13 + .../widgets/form_renderer/form_renderer.js | 199 + .../widgets/form_renderer/form_renderer.scss | 17 + .../form_renderer/form_renderer_tests.js | 455 ++ .../widgets/messaging_menu/messaging_menu.js | 56 + .../widgets/messaging_menu/messaging_menu.xml | 8 + .../static/src/xml/abstract_thread_window.xml | 85 - addons/mail/static/src/xml/chatter.xml | 152 - addons/mail/static/src/xml/composer.xml | 91 +- addons/mail/static/src/xml/discuss.xml | 352 -- addons/mail/static/src/xml/followers.xml | 95 - addons/mail/static/src/xml/out_of_office.xml | 9 - addons/mail/static/src/xml/systray.xml | 58 - addons/mail/static/src/xml/text_emojis.xml | 2 +- addons/mail/static/src/xml/thread.xml | 536 -- addons/mail/static/src/xml/thread_window.xml | 32 - addons/mail/static/tests/chatter_tests.js | 3138 +--------- .../static/tests/discuss_moderation_tests.js | 843 --- .../tests/discuss_seen_indicator_tests.js | 1121 ---- addons/mail/static/tests/discuss_tests.js | 2076 ------- .../discuss_typing_notification_tests.js | 702 --- .../tests/document_thread_window_tests.js | 325 - .../mail/static/tests/helpers/mock_server.js | 109 +- .../mail/static/tests/helpers/test_utils.js | 220 - .../static/tests/mail_status_manager_tests.js | 190 - .../tests/many2one_avatar_user_tests.js | 29 +- .../models/cc_throttle_function_tests.js | 154 - .../mail/static/tests/models/timer_tests.js | 79 - .../mail/static/tests/models/timers_tests.js | 90 - .../systray/systray_activity_menu_tests.js | 59 +- ...stray_messaging_menu_mail_failure_tests.js | 216 - .../systray/systray_messaging_menu_tests.js | 1153 ---- .../basic_thread_window_tests.js | 856 --- .../blank_thread_window_tests.js | 325 - .../hidden_thread_window_tests.js | 322 - .../moderation_thread_window_tests.js | 138 - .../typing_thread_window_tests.js | 131 - addons/mail/views/assets.xml | 279 +- addons/mail/views/mail_blacklist_views.xml | 4 +- addons/mail/views/mail_channel_views.xml | 19 +- addons/mail/views/res_partner_views.xml | 6 +- addons/mail/wizard/invite.py | 6 + addons/mail_bot/__manifest__.py | 6 +- addons/mail_bot/static/src/bugfix/bugfix.js | 10 + addons/mail_bot/static/src/bugfix/bugfix.scss | 6 + addons/mail_bot/static/src/bugfix/bugfix.xml | 11 + .../static/src/bugfix/bugfix_tests.js | 18 + .../messaging_menu/messaging_menu.js | 27 + .../messaging_menu/messaging_menu.xml | 8 + .../messaging_menu/messaging_menu_tests.js | 202 + .../notification_alert/notification_alert.js | 52 + .../notification_alert/notification_alert.xml | 14 + .../notification_list/notification_list.js | 44 + .../notification_list/notification_list.xml | 13 + .../notification_request.js | 90 + .../notification_request.scss | 73 + .../notification_request.xml | 31 + .../mail_bot/static/src/js/mailbot_service.js | 88 - .../static/src/js/notification_alert.js | 26 - .../static/src/js/systray_messaging_menu.js | 119 - .../static/src/models/messaging/messaging.js | 22 + .../messaging_initializer.js | 35 + .../models/messaging_menu/messaging_menu.js | 28 + .../notification_alert/notification_alert.js | 45 + .../notification_alert_tests.js | 109 + .../tests/systray_messaging_menu_tests.js | 195 - addons/mail_bot/views/assets.xml | 24 +- addons/mail_bot/views/discuss.xml | 17 - .../maintenance/views/maintenance_views.xml | 16 +- .../views/mailing_contact_views.xml | 4 +- .../views/mailing_mailing_views.xml | 6 +- addons/mrp/views/mrp_bom_views.xml | 4 +- addons/mrp/views/mrp_production_views.xml | 6 +- addons/mrp/views/mrp_unbuild_views.xml | 6 +- addons/mrp/views/mrp_workorder_views.xml | 6 +- .../tests/systray_activity_menu_tests.js | 14 +- addons/note/views/note_views.xml | 6 +- .../views/phone_blacklist_views.xml | 4 +- .../point_of_sale/views/pos_assets_common.xml | 1 - .../point_of_sale/views/pos_session_view.xml | 6 +- addons/product/views/product_views.xml | 6 +- addons/project/static/src/js/tours/project.js | 6 +- addons/project/views/project_views.xml | 8 +- addons/purchase/views/purchase_views.xml | 6 +- .../views/purchase_requisition_views.xml | 6 +- addons/repair/views/repair_views.xml | 6 +- addons/sale/views/sale_views.xml | 6 +- addons/sales_team/views/crm_team_views.xml | 4 +- addons/sms/__manifest__.py | 4 +- addons/sms/static/src/bugfix/bugfix.js | 10 + addons/sms/static/src/bugfix/bugfix.scss | 6 + addons/sms/static/src/bugfix/bugfix.xml | 11 + addons/sms/static/src/bugfix/bugfix_tests.js | 18 + .../static/src/components/message/message.xml | 20 + .../src/components/message/message_tests.js | 187 + .../notification_group/notification_group.js | 27 + .../notification_group/notification_group.xml | 12 + ...ification_list_notification_group_tests.js | 312 + addons/sms/static/src/js/mail_failure.js | 51 - addons/sms/static/src/js/message.js | 32 - .../static/src/js/systray_messaging_menu.js | 80 - addons/sms/static/src/js/thread_widget.js | 27 - .../sms/static/src/models/message/message.js | 33 + .../notification_group/notification_group.js | 57 + addons/sms/static/src/xml/thread.xml | 8 - addons/sms/views/assets.xml | 15 +- addons/snailmail/__manifest__.py | 6 +- addons/snailmail/static/src/bugfix/bugfix.js | 10 + .../snailmail/static/src/bugfix/bugfix.scss | 6 + addons/snailmail/static/src/bugfix/bugfix.xml | 11 + .../static/src/bugfix/bugfix_tests.js | 18 + .../static/src/components/message/message.js | 87 + .../static/src/components/message/message.xml | 37 + .../src/components/message/message_tests.js | 624 ++ .../notification_group/notification_group.js | 27 + .../notification_group/notification_group.xml | 12 + ...ification_list_notification_group_tests.js | 312 + .../snailmail_error_dialog.js | 113 + .../snailmail_error_dialog.xml | 61 + .../snailmail_notification_popover.js | 86 + .../snailmail_notification_popover.scss | 7 + .../snailmail_notification_popover.xml | 13 + .../snailmail/static/src/js/mail_failure.js | 51 - addons/snailmail/static/src/js/message.js | 48 - .../static/src/js/systray_messaging_menu.js | 78 - .../snailmail/static/src/js/thread_widget.js | 215 - .../static/src/models/message/message.js | 69 + .../static/src/models/messaging/messaging.js | 38 + .../notification_group/notification_group.js | 57 + addons/snailmail/static/src/xml/thread.xml | 49 - .../static/tests/chatter_snailmail_tests.js | 509 -- addons/snailmail/views/assets.xml | 19 +- addons/stock/views/stock_picking_views.xml | 6 +- .../views/stock_production_lot_views.xml | 6 +- addons/stock/views/stock_scrap_views.xml | 4 +- .../views/stock_landed_cost_views.xml | 6 +- .../views/stock_picking_batch_views.xml | 6 +- addons/survey/views/survey_survey_views.xml | 6 +- addons/web/static/src/js/common_env.js | 17 + addons/web/static/src/js/core/rpc.js | 14 +- addons/web/static/src/js/owl_compatibility.js | 7 +- addons/web/static/src/js/tools/test_menus.js | 13 +- .../src/js/views/basic/basic_renderer.js | 3 + .../static/src/js/views/form/form_renderer.js | 14 +- .../web/static/src/js/views/form/form_view.js | 1 + .../web/static/src/js/views/view_dialogs.js | 1 + addons/web/static/src/scss/form_view.scss | 3 - .../web/static/src/scss/form_view_extra.scss | 12 +- .../static/src/scss/primary_variables.scss | 4 + addons/web/static/tests/helpers/test_env.js | 9 + .../static/tests/helpers/test_utils_mock.js | 5 +- .../static/tests/tours/reset_password.js | 7 - .../website_blog/views/website_blog_views.xml | 8 +- .../views/event_sponsor_views.xml | 6 +- .../views/event_track_views.xml | 6 +- addons/website_forum/views/forum.xml | 12 +- addons/website_livechat/__manifest__.py | 3 +- addons/website_livechat/controllers/main.py | 9 - .../static/src/bugfix/bugfix.js | 10 + .../static/src/bugfix/bugfix.scss | 6 + .../static/src/bugfix/bugfix.xml | 11 + .../static/src/bugfix/bugfix_tests.js | 18 + .../static/src/bugfix/public_bugfix.js | 10 + .../static/src/bugfix/public_bugfix.scss | 6 + .../static/src/bugfix/public_bugfix.xml | 11 + .../website_livechat/static/src/js/channel.js | 34 - .../static/src/js/thread_window.js | 34 - .../public_livechat.js} | 4 +- .../src/scss/mail_thread_visitor_info.scss | 34 - .../static/src/xml/thread.xml | 60 - .../tests/tours/website_livechat_common.js | 2 +- addons/website_livechat/views/assets.xml | 16 + .../views/website_livechat.xml | 32 +- .../tours/website_sale_shop_cart_recovery.js | 4 +- .../tests/tours/website_sale_shop_mail.js | 2 +- .../website_slides/static/src/js/activity.js | 2 - .../views/slide_channel_views.xml | 6 +- .../views/slide_slide_views.xml | 4 +- 564 files changed, 56243 insertions(+), 33378 deletions(-) create mode 100644 addons/hr_holidays/static/src/bugfix/bugfix.js create mode 100644 addons/hr_holidays/static/src/bugfix/bugfix.scss create mode 100644 addons/hr_holidays/static/src/bugfix/bugfix.xml create mode 100644 addons/hr_holidays/static/src/bugfix/bugfix_tests.js create mode 100644 addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss create mode 100644 addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml create mode 100644 addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss create mode 100644 addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml create mode 100644 addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.js create mode 100644 addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.scss create mode 100644 addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.xml delete mode 100644 addons/hr_holidays/static/src/xml/discuss.xml create mode 100644 addons/im_livechat/static/src/bugfix/bugfix.js create mode 100644 addons/im_livechat/static/src/bugfix/bugfix.scss create mode 100644 addons/im_livechat/static/src/bugfix/bugfix.xml create mode 100644 addons/im_livechat/static/src/bugfix/bugfix_tests.js create mode 100644 addons/im_livechat/static/src/components/discuss/discuss.js create mode 100644 addons/im_livechat/static/src/components/discuss/discuss_tests.js create mode 100644 addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js create mode 100644 addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml create mode 100644 addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js create mode 100644 addons/im_livechat/static/src/components/notification_list/notification_list.js create mode 100644 addons/im_livechat/static/src/components/thread_icon/thread_icon.xml create mode 100644 addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js create mode 100644 addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js create mode 100644 addons/im_livechat/static/src/components/thread_preview/thread_preview.js create mode 100644 addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js delete mode 100644 addons/im_livechat/static/src/js/im_livechat.js delete mode 100644 addons/im_livechat/static/src/js/im_livechat_backend.js delete mode 100644 addons/im_livechat/static/src/js/models/website_livechat.js delete mode 100644 addons/im_livechat/static/src/js/models/website_livechat_message.js delete mode 100644 addons/im_livechat/static/src/js/website_livechat_window.js create mode 100644 addons/im_livechat/static/src/legacy/public_livechat.js create mode 100644 addons/im_livechat/static/src/legacy/public_livechat.scss create mode 100644 addons/im_livechat/static/src/legacy/public_livechat.xml create mode 100644 addons/im_livechat/static/src/models/messaging_initializer/messaging_initializer.js create mode 100644 addons/im_livechat/static/src/models/messaging_notification_handler/messaging_notification_handler.js create mode 100644 addons/im_livechat/static/src/models/thread/thread.js delete mode 100644 addons/im_livechat/static/src/scss/im_livechat.scss delete mode 100644 addons/im_livechat/static/src/xml/im_livechat.xml delete mode 100644 addons/im_livechat/static/src/xml/im_livechat_backend.xml delete mode 100644 addons/im_livechat/static/tests/discuss_livechat_tests.js delete mode 100644 addons/im_livechat/static/tests/website_livechat_window_tests.js create mode 100644 addons/mail/static/src/bugfix/bugfix.js create mode 100644 addons/mail/static/src/bugfix/bugfix.scss create mode 100644 addons/mail/static/src/bugfix/bugfix.xml create mode 100644 addons/mail/static/src/bugfix/bugfix_tests.js create mode 100644 addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js create mode 100644 addons/mail/static/src/component_hooks/use_refs/use_refs.js create mode 100644 addons/mail/static/src/component_hooks/use_store/use_store.js create mode 100644 addons/mail/static/src/component_hooks/use_store/use_store_tests.js create mode 100644 addons/mail/static/src/components/activity/activity.js create mode 100644 addons/mail/static/src/components/activity/activity.scss create mode 100644 addons/mail/static/src/components/activity/activity.xml create mode 100644 addons/mail/static/src/components/activity/activity_tests.js create mode 100644 addons/mail/static/src/components/activity_box/activity_box.js create mode 100644 addons/mail/static/src/components/activity_box/activity_box.scss create mode 100644 addons/mail/static/src/components/activity_box/activity_box.xml create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.scss create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.xml create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover_tests.js create mode 100644 addons/mail/static/src/components/attachment/attachment.js create mode 100644 addons/mail/static/src/components/attachment/attachment.scss create mode 100644 addons/mail/static/src/components/attachment/attachment.xml create mode 100644 addons/mail/static/src/components/attachment/attachment_tests.js create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.js create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.scss create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.xml create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box_tests.js create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.js create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.scss create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.xml create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.js create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.scss create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml create mode 100644 addons/mail/static/src/components/autocomplete_input/autocomplete_input.js create mode 100644 addons/mail/static/src/components/autocomplete_input/autocomplete_input.xml create mode 100644 addons/mail/static/src/components/chat_window/chat_window.js create mode 100644 addons/mail/static/src/components/chat_window/chat_window.scss create mode 100644 addons/mail/static/src/components/chat_window/chat_window.xml create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.js create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.scss create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.xml create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.scss create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.xml create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.js create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.scss create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js create mode 100644 addons/mail/static/src/components/chatter/chatter.js create mode 100644 addons/mail/static/src/components/chatter/chatter.scss create mode 100644 addons/mail/static/src/components/chatter/chatter.xml create mode 100644 addons/mail/static/src/components/chatter/chatter_tests.js create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.js create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.scss create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.xml create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.js create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.scss create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js create mode 100644 addons/mail/static/src/components/composer/composer.js create mode 100644 addons/mail/static/src/components/composer/composer.scss create mode 100644 addons/mail/static/src/components/composer/composer.xml create mode 100644 addons/mail/static/src/components/composer/composer_tests.js create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.js create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.scss create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.xml create mode 100644 addons/mail/static/src/components/dialog/dialog.js create mode 100644 addons/mail/static/src/components/dialog/dialog.scss create mode 100644 addons/mail/static/src/components/dialog/dialog.xml create mode 100644 addons/mail/static/src/components/dialog_manager/dialog_manager.js create mode 100644 addons/mail/static/src/components/dialog_manager/dialog_manager.xml create mode 100644 addons/mail/static/src/components/discuss/discuss.js create mode 100644 addons/mail/static/src/components/discuss/discuss.scss create mode 100644 addons/mail/static/src/components/discuss/discuss.xml create mode 100644 addons/mail/static/src/components/discuss/discuss_tests.js create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.scss create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.xml create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.scss create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.xml create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.scss create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.xml create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.js create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.scss create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.xml create mode 100644 addons/mail/static/src/components/editable_text/editable_text.js create mode 100644 addons/mail/static/src/components/editable_text/editable_text.xml create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.js create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.scss create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.xml create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.js create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.scss create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.xml create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader_tests.js create mode 100644 addons/mail/static/src/components/follow_button/follow_button.js create mode 100644 addons/mail/static/src/components/follow_button/follow_button.scss create mode 100644 addons/mail/static/src/components/follow_button/follow_button.xml create mode 100644 addons/mail/static/src/components/follow_button/follow_button_tests.js create mode 100644 addons/mail/static/src/components/follower/follower.js create mode 100644 addons/mail/static/src/components/follower/follower.scss create mode 100644 addons/mail/static/src/components/follower/follower.xml create mode 100644 addons/mail/static/src/components/follower/follower_tests.js create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.js create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.scss create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu_tests.js create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.js create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.scss create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.xml create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype_tests.js create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.js create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.scss create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.xml create mode 100644 addons/mail/static/src/components/mail_template/mail_template.js create mode 100644 addons/mail/static/src/components/mail_template/mail_template.scss create mode 100644 addons/mail/static/src/components/mail_template/mail_template.xml create mode 100644 addons/mail/static/src/components/message/message.js create mode 100644 addons/mail/static/src/components/message/message.scss create mode 100644 addons/mail/static/src/components/message/message.xml create mode 100644 addons/mail/static/src/components/message/message_tests.js create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.js create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.scss create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml create mode 100644 addons/mail/static/src/components/message_list/message_list.js create mode 100644 addons/mail/static/src/components/message_list/message_list.scss create mode 100644 addons/mail/static/src/components/message_list/message_list.xml create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.js create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.scss create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.xml create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.scss create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.xml create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.scss create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.xml create mode 100644 addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js create mode 100644 addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.xml create mode 100644 addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js create mode 100644 addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml create mode 100644 addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js create mode 100644 addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.xml create mode 100644 addons/mail/static/src/components/notification_group/notification_group.js create mode 100644 addons/mail/static/src/components/notification_group/notification_group.scss create mode 100644 addons/mail/static/src/components/notification_group/notification_group.xml create mode 100644 addons/mail/static/src/components/notification_list/notification_list.js create mode 100644 addons/mail/static/src/components/notification_list/notification_list.scss create mode 100644 addons/mail/static/src/components/notification_list/notification_list.xml create mode 100644 addons/mail/static/src/components/notification_list/notification_list_item.scss create mode 100644 addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js create mode 100644 addons/mail/static/src/components/notification_list/notification_list_tests.js create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.js create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.scss create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.xml create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.scss create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.xml create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon_tests.js create mode 100644 addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js create mode 100644 addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.scss create mode 100644 addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml create mode 100644 addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.js create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.scss create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.xml create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon_tests.js create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.scss create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.js create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.scss create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.xml create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.scss create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.xml create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.js create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.scss create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.xml create mode 100644 addons/mail/static/src/components/thread_viewer/thread_viewer.js create mode 100644 addons/mail/static/src/components/thread_viewer/thread_viewer.scss create mode 100644 addons/mail/static/src/components/thread_viewer/thread_viewer.xml create mode 100644 addons/mail/static/src/components/thread_viewer/thread_viewer_tests.js create mode 100644 addons/mail/static/src/env/env.js create mode 100644 addons/mail/static/src/env/test_env.js delete mode 100644 addons/mail/static/src/js/attachment_box.js delete mode 100644 addons/mail/static/src/js/basic_model.js delete mode 100644 addons/mail/static/src/js/chatter.js delete mode 100644 addons/mail/static/src/js/composers/basic_composer.js delete mode 100644 addons/mail/static/src/js/composers/chatter_composer.js delete mode 100644 addons/mail/static/src/js/composers/extended_composer.js delete mode 100644 addons/mail/static/src/js/composers/mention_manager.js delete mode 100644 addons/mail/static/src/js/discuss.js delete mode 100644 addons/mail/static/src/js/followers.js delete mode 100644 addons/mail/static/src/js/form_controller.js delete mode 100644 addons/mail/static/src/js/form_renderer.js delete mode 100644 addons/mail/static/src/js/form_view.js delete mode 100644 addons/mail/static/src/js/models/mail_failure.js delete mode 100644 addons/mail/static/src/js/models/messages/abstract_message.js delete mode 100644 addons/mail/static/src/js/models/messages/message.js delete mode 100644 addons/mail/static/src/js/models/threads/abstract_thread.js delete mode 100644 addons/mail/static/src/js/models/threads/channel.js delete mode 100644 addons/mail/static/src/js/models/threads/create_mode_document_thread.js delete mode 100644 addons/mail/static/src/js/models/threads/dm_chat.js delete mode 100644 addons/mail/static/src/js/models/threads/document_thread.js delete mode 100644 addons/mail/static/src/js/models/threads/livechat.js delete mode 100644 addons/mail/static/src/js/models/threads/mailbox.js delete mode 100644 addons/mail/static/src/js/models/threads/mixins/channel_seen_mixin.js delete mode 100644 addons/mail/static/src/js/models/threads/mixins/thread_typing_mixin.js delete mode 100644 addons/mail/static/src/js/models/threads/multi_user_channel.js delete mode 100644 addons/mail/static/src/js/models/threads/searchable_thread.js delete mode 100644 addons/mail/static/src/js/models/threads/thread.js delete mode 100644 addons/mail/static/src/js/models/threads/two_user_channel.js delete mode 100644 addons/mail/static/src/js/models/utils/cc_throttle_function.js delete mode 100644 addons/mail/static/src/js/models/utils/timer.js delete mode 100644 addons/mail/static/src/js/models/utils/timers.js delete mode 100644 addons/mail/static/src/js/services/mail_document_thread_manager.js delete mode 100644 addons/mail/static/src/js/services/mail_manager.js delete mode 100644 addons/mail/static/src/js/services/mail_notification_manager.js delete mode 100644 addons/mail/static/src/js/services/mail_service.js delete mode 100644 addons/mail/static/src/js/services/mail_status_manager.js delete mode 100644 addons/mail/static/src/js/services/mail_window_manager.js delete mode 100644 addons/mail/static/src/js/systray/systray_messaging_menu.js delete mode 100644 addons/mail/static/src/js/thread_field.js delete mode 100644 addons/mail/static/src/js/thread_widget.js delete mode 100644 addons/mail/static/src/js/thread_windows/abstract_thread_window.js delete mode 100644 addons/mail/static/src/js/thread_windows/thread_window.js create mode 100644 addons/mail/static/src/model/model_core.js create mode 100644 addons/mail/static/src/model/model_errors.js create mode 100644 addons/mail/static/src/model/model_field.js create mode 100644 addons/mail/static/src/model/model_manager.js create mode 100644 addons/mail/static/src/models/activity/activity.js create mode 100644 addons/mail/static/src/models/activity_type/activity_type.js create mode 100644 addons/mail/static/src/models/attachment/attachment.js create mode 100644 addons/mail/static/src/models/attachment/attachment_tests.js create mode 100644 addons/mail/static/src/models/attachment_viewer/attachment_viewer.js create mode 100644 addons/mail/static/src/models/chat_window/chat_window.js create mode 100644 addons/mail/static/src/models/chat_window_manager/chat_window_manager.js create mode 100644 addons/mail/static/src/models/chatter/chatter.js create mode 100644 addons/mail/static/src/models/composer/composer.js create mode 100644 addons/mail/static/src/models/country/country.js create mode 100644 addons/mail/static/src/models/device/device.js create mode 100644 addons/mail/static/src/models/dialog/dialog.js create mode 100644 addons/mail/static/src/models/dialog_manager/dialog_manager.js create mode 100644 addons/mail/static/src/models/discuss/discuss.js create mode 100644 addons/mail/static/src/models/follower/follower.js create mode 100644 addons/mail/static/src/models/follower_subtype/follower_subtype.js create mode 100644 addons/mail/static/src/models/follower_subtype_list/follower_subtype_list.js create mode 100644 addons/mail/static/src/models/locale/locale.js create mode 100644 addons/mail/static/src/models/mail_template/mail_template.js create mode 100644 addons/mail/static/src/models/message/message.js create mode 100644 addons/mail/static/src/models/message/message_tests.js create mode 100644 addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js create mode 100644 addons/mail/static/src/models/messaging/messaging.js create mode 100644 addons/mail/static/src/models/messaging/messaging_tests.js create mode 100644 addons/mail/static/src/models/messaging_initializer/messaging_initializer.js create mode 100644 addons/mail/static/src/models/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js create mode 100644 addons/mail/static/src/models/model/model.js create mode 100644 addons/mail/static/src/models/notification/notification.js create mode 100644 addons/mail/static/src/models/notification_group/notification_group.js create mode 100644 addons/mail/static/src/models/notification_group_manager/notification_group_manager.js create mode 100644 addons/mail/static/src/models/partner/partner.js create mode 100644 addons/mail/static/src/models/thread/thread.js create mode 100644 addons/mail/static/src/models/thread/thread_tests.js create mode 100644 addons/mail/static/src/models/thread_cache/thread_cache.js create mode 100644 addons/mail/static/src/models/thread_partner_seen_info/thread_partner_seen_info.js create mode 100644 addons/mail/static/src/models/thread_viewer/thread_viewer.js create mode 100644 addons/mail/static/src/models/user/user.js delete mode 100644 addons/mail/static/src/scss/abstract_thread_window.scss delete mode 100644 addons/mail/static/src/scss/attachment_box.scss delete mode 100644 addons/mail/static/src/scss/chatter.scss delete mode 100644 addons/mail/static/src/scss/followers.scss delete mode 100644 addons/mail/static/src/scss/out_of_office.scss delete mode 100644 addons/mail/static/src/scss/thread_window.scss create mode 100644 addons/mail/static/src/services/chat_window_service/chat_window_service.js create mode 100644 addons/mail/static/src/services/dialog_service/dialog_service.js create mode 100644 addons/mail/static/src/utils/deferred/deferred.js create mode 100644 addons/mail/static/src/utils/test_utils.js create mode 100644 addons/mail/static/src/utils/throttle/throttle.js create mode 100644 addons/mail/static/src/utils/throttle/throttle_tests.js create mode 100644 addons/mail/static/src/utils/timer/timer.js create mode 100644 addons/mail/static/src/utils/timer/timer_tests.js create mode 100644 addons/mail/static/src/utils/utils.js create mode 100644 addons/mail/static/src/variables.scss create mode 100644 addons/mail/static/src/widgets/common.xml create mode 100644 addons/mail/static/src/widgets/discuss/discuss.js create mode 100644 addons/mail/static/src/widgets/discuss/discuss.scss create mode 100644 addons/mail/static/src/widgets/discuss/discuss.xml create mode 100644 addons/mail/static/src/widgets/discuss_invite_partner_dialog/discuss_invite_partner_dialog.js create mode 100644 addons/mail/static/src/widgets/discuss_invite_partner_dialog/discuss_invite_partner_dialog.xml create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer.js create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer.scss create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js create mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml delete mode 100644 addons/mail/static/src/xml/abstract_thread_window.xml delete mode 100644 addons/mail/static/src/xml/chatter.xml delete mode 100644 addons/mail/static/src/xml/discuss.xml delete mode 100644 addons/mail/static/src/xml/followers.xml delete mode 100644 addons/mail/static/src/xml/out_of_office.xml delete mode 100644 addons/mail/static/src/xml/thread_window.xml delete mode 100644 addons/mail/static/tests/discuss_moderation_tests.js delete mode 100644 addons/mail/static/tests/discuss_seen_indicator_tests.js delete mode 100644 addons/mail/static/tests/discuss_tests.js delete mode 100644 addons/mail/static/tests/discuss_typing_notification_tests.js delete mode 100644 addons/mail/static/tests/document_thread_window_tests.js delete mode 100644 addons/mail/static/tests/helpers/test_utils.js delete mode 100644 addons/mail/static/tests/mail_status_manager_tests.js delete mode 100644 addons/mail/static/tests/models/cc_throttle_function_tests.js delete mode 100644 addons/mail/static/tests/models/timer_tests.js delete mode 100644 addons/mail/static/tests/models/timers_tests.js delete mode 100644 addons/mail/static/tests/systray/systray_messaging_menu_mail_failure_tests.js delete mode 100644 addons/mail/static/tests/systray/systray_messaging_menu_tests.js delete mode 100644 addons/mail/static/tests/thread_window/basic_thread_window_tests.js delete mode 100644 addons/mail/static/tests/thread_window/blank_thread_window_tests.js delete mode 100644 addons/mail/static/tests/thread_window/hidden_thread_window_tests.js delete mode 100644 addons/mail/static/tests/thread_window/moderation_thread_window_tests.js delete mode 100644 addons/mail/static/tests/thread_window/typing_thread_window_tests.js create mode 100644 addons/mail_bot/static/src/bugfix/bugfix.js create mode 100644 addons/mail_bot/static/src/bugfix/bugfix.scss create mode 100644 addons/mail_bot/static/src/bugfix/bugfix.xml create mode 100644 addons/mail_bot/static/src/bugfix/bugfix_tests.js create mode 100644 addons/mail_bot/static/src/components/messaging_menu/messaging_menu.js create mode 100644 addons/mail_bot/static/src/components/messaging_menu/messaging_menu.xml create mode 100644 addons/mail_bot/static/src/components/messaging_menu/messaging_menu_tests.js create mode 100644 addons/mail_bot/static/src/components/notification_alert/notification_alert.js create mode 100644 addons/mail_bot/static/src/components/notification_alert/notification_alert.xml create mode 100644 addons/mail_bot/static/src/components/notification_list/notification_list.js create mode 100644 addons/mail_bot/static/src/components/notification_list/notification_list.xml create mode 100644 addons/mail_bot/static/src/components/notification_request/notification_request.js create mode 100644 addons/mail_bot/static/src/components/notification_request/notification_request.scss create mode 100644 addons/mail_bot/static/src/components/notification_request/notification_request.xml delete mode 100644 addons/mail_bot/static/src/js/mailbot_service.js delete mode 100644 addons/mail_bot/static/src/js/notification_alert.js delete mode 100644 addons/mail_bot/static/src/js/systray_messaging_menu.js create mode 100644 addons/mail_bot/static/src/models/messaging/messaging.js create mode 100644 addons/mail_bot/static/src/models/messaging_initializer/messaging_initializer.js create mode 100644 addons/mail_bot/static/src/models/messaging_menu/messaging_menu.js create mode 100644 addons/mail_bot/static/src/widgets/notification_alert/notification_alert.js create mode 100644 addons/mail_bot/static/src/widgets/notification_alert/notification_alert_tests.js delete mode 100644 addons/mail_bot/static/tests/systray_messaging_menu_tests.js delete mode 100644 addons/mail_bot/views/discuss.xml create mode 100644 addons/sms/static/src/bugfix/bugfix.js create mode 100644 addons/sms/static/src/bugfix/bugfix.scss create mode 100644 addons/sms/static/src/bugfix/bugfix.xml create mode 100644 addons/sms/static/src/bugfix/bugfix_tests.js create mode 100644 addons/sms/static/src/components/message/message.xml create mode 100644 addons/sms/static/src/components/message/message_tests.js create mode 100644 addons/sms/static/src/components/notification_group/notification_group.js create mode 100644 addons/sms/static/src/components/notification_group/notification_group.xml create mode 100644 addons/sms/static/src/components/notification_list/notification_list_notification_group_tests.js delete mode 100644 addons/sms/static/src/js/mail_failure.js delete mode 100644 addons/sms/static/src/js/message.js delete mode 100644 addons/sms/static/src/js/systray_messaging_menu.js delete mode 100644 addons/sms/static/src/js/thread_widget.js create mode 100644 addons/sms/static/src/models/message/message.js create mode 100644 addons/sms/static/src/models/notification_group/notification_group.js delete mode 100644 addons/sms/static/src/xml/thread.xml create mode 100644 addons/snailmail/static/src/bugfix/bugfix.js create mode 100644 addons/snailmail/static/src/bugfix/bugfix.scss create mode 100644 addons/snailmail/static/src/bugfix/bugfix.xml create mode 100644 addons/snailmail/static/src/bugfix/bugfix_tests.js create mode 100644 addons/snailmail/static/src/components/message/message.js create mode 100644 addons/snailmail/static/src/components/message/message.xml create mode 100644 addons/snailmail/static/src/components/message/message_tests.js create mode 100644 addons/snailmail/static/src/components/notification_group/notification_group.js create mode 100644 addons/snailmail/static/src/components/notification_group/notification_group.xml create mode 100644 addons/snailmail/static/src/components/notification_list/notification_list_notification_group_tests.js create mode 100644 addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.js create mode 100644 addons/snailmail/static/src/components/snailmail_error_dialog/snailmail_error_dialog.xml create mode 100644 addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.js create mode 100644 addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.scss create mode 100644 addons/snailmail/static/src/components/snailmail_notification_popover/snailmail_notification_popover.xml delete mode 100644 addons/snailmail/static/src/js/mail_failure.js delete mode 100644 addons/snailmail/static/src/js/message.js delete mode 100644 addons/snailmail/static/src/js/systray_messaging_menu.js delete mode 100644 addons/snailmail/static/src/js/thread_widget.js create mode 100644 addons/snailmail/static/src/models/message/message.js create mode 100644 addons/snailmail/static/src/models/messaging/messaging.js create mode 100644 addons/snailmail/static/src/models/notification_group/notification_group.js delete mode 100644 addons/snailmail/static/src/xml/thread.xml delete mode 100644 addons/snailmail/static/tests/chatter_snailmail_tests.js create mode 100644 addons/website_livechat/static/src/bugfix/bugfix.js create mode 100644 addons/website_livechat/static/src/bugfix/bugfix.scss create mode 100644 addons/website_livechat/static/src/bugfix/bugfix.xml create mode 100644 addons/website_livechat/static/src/bugfix/bugfix_tests.js create mode 100644 addons/website_livechat/static/src/bugfix/public_bugfix.js create mode 100644 addons/website_livechat/static/src/bugfix/public_bugfix.scss create mode 100644 addons/website_livechat/static/src/bugfix/public_bugfix.xml delete mode 100644 addons/website_livechat/static/src/js/channel.js delete mode 100644 addons/website_livechat/static/src/js/thread_window.js rename addons/website_livechat/static/src/{js/im_livechat.js => legacy/public_livechat.js} (88%) delete mode 100644 addons/website_livechat/static/src/scss/mail_thread_visitor_info.scss delete mode 100644 addons/website_livechat/static/src/xml/thread.xml diff --git a/addons/account/views/account_bank_statement_views.xml b/addons/account/views/account_bank_statement_views.xml index be075e890c8..d447dd16bad 100644 --- a/addons/account/views/account_bank_statement_views.xml +++ b/addons/account/views/account_bank_statement_views.xml @@ -231,8 +231,8 @@
- - + +
diff --git a/addons/account/views/account_journal_views.xml b/addons/account/views/account_journal_views.xml index 06a332f0bad..83f2a5d9992 100644 --- a/addons/account/views/account_journal_views.xml +++ b/addons/account/views/account_journal_views.xml @@ -122,9 +122,9 @@
- - - + + +
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 76c09bb8509..ae9fead3ac6 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -1049,9 +1049,9 @@ ('state', '!=', 'draft')]}" />
- - - + + +
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index a8e875c4116..cf335bb0fdc 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -247,9 +247,9 @@
- - - + + +
diff --git a/addons/analytic/views/analytic_account_views.xml b/addons/analytic/views/analytic_account_views.xml index 6d768433a7e..972bcaa6421 100644 --- a/addons/analytic/views/analytic_account_views.xml +++ b/addons/analytic/views/analytic_account_views.xml @@ -182,8 +182,8 @@
- - + +
diff --git a/addons/calendar/static/tests/systray_activity_menu_tests.js b/addons/calendar/static/tests/systray_activity_menu_tests.js index 02937bae0ed..1d50f011ac1 100644 --- a/addons/calendar/static/tests/systray_activity_menu_tests.js +++ b/addons/calendar/static/tests/systray_activity_menu_tests.js @@ -1,8 +1,8 @@ odoo.define('calendar.systray.ActivityMenuTests', function (require) { "use strict"; +const { start } = require('mail/static/src/utils/test_utils.js'); var ActivityMenu = require('mail.systray.ActivityMenu'); -var mailUtils = require('mail.testUtils'); var testUtils = require('web.test_utils'); @@ -11,7 +11,6 @@ QUnit.module('calendar', {}, function () { QUnit.module('ActivityMenu', { beforeEach: function () { - this.services = mailUtils.getMailServices(); this.data = { 'calendar.event': { records: [{ @@ -34,15 +33,14 @@ QUnit.module('ActivityMenu', { }], }, }; - } + }, }); QUnit.test('activity menu widget:today meetings', async function (assert) { assert.expect(6); var self = this; - var activityMenu = new ActivityMenu(); - await testUtils.mock.addMockEnvironment(activityMenu, { - services: this.services, + + const { widget } = await start({ mockRPC: function (route, args) { if (args.method === 'systray_get_activities') { return Promise.resolve(self.data['calendar.event']['records']); @@ -51,6 +49,7 @@ QUnit.test('activity menu widget:today meetings', async function (assert) { }, }); + const activityMenu = new ActivityMenu(widget); await activityMenu.appendTo($('#qunit-fixture')); assert.hasClass(activityMenu.$el,'o_mail_systray_item', 'should be the instance of widget'); @@ -66,7 +65,7 @@ QUnit.test('activity menu widget:today meetings', async function (assert) { assert.containsN(activityMenu, '.o_meeting_filter', 2, 'there should be 2 meetings'); assert.hasClass(activityMenu.$('.o_meeting_filter').eq(0), 'o_meeting_bold', 'this meeting is yet to start'); assert.doesNotHaveClass(activityMenu.$('.o_meeting_filter').eq(1), 'o_meeting_bold', 'this meeting has been started'); - activityMenu.destroy(); + widget.destroy(); }); }); }); diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml index fff23a6d3f6..478114f7d59 100644 --- a/addons/calendar/views/calendar_views.xml +++ b/addons/calendar/views/calendar_views.xml @@ -239,8 +239,8 @@
- - + +
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index ce4f0bbc754..a2666eb3cff 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -286,9 +286,9 @@
- - - + + +
diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index 4ce4a68b607..52da1fce825 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -271,9 +271,9 @@
- - - + + +
@@ -562,9 +562,9 @@
- - - + + +
diff --git a/addons/fleet/views/fleet_vehicle_cost_views.xml b/addons/fleet/views/fleet_vehicle_cost_views.xml index de9c69f1e27..929439b6abe 100644 --- a/addons/fleet/views/fleet_vehicle_cost_views.xml +++ b/addons/fleet/views/fleet_vehicle_cost_views.xml @@ -54,9 +54,9 @@
- - - + + +
@@ -228,9 +228,9 @@
- - - + + +
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml index 07e40d1e0b6..c0765013e87 100644 --- a/addons/fleet/views/fleet_vehicle_views.xml +++ b/addons/fleet/views/fleet_vehicle_views.xml @@ -119,9 +119,9 @@
- - - + + +
diff --git a/addons/gamification/views/challenge.xml b/addons/gamification/views/challenge.xml index b5f4e038844..66a4b400ead 100644 --- a/addons/gamification/views/challenge.xml +++ b/addons/gamification/views/challenge.xml @@ -130,8 +130,8 @@
- - + +
diff --git a/addons/hr/static/src/js/chat.js b/addons/hr/static/src/js/chat.js index bcffe4ca47d..b9985e3929f 100644 --- a/addons/hr/static/src/js/chat.js +++ b/addons/hr/static/src/js/chat.js @@ -11,6 +11,8 @@ odoo.define('hr.employee_chat', function (require) { var KanbanRenderer = require('web.KanbanRenderer'); var KanbanRecord = require('web.KanbanRecord'); + const { Component } = owl; + // CHAT MIXIN var ChatMixin = { /** @@ -22,8 +24,7 @@ odoo.define('hr.employee_chat', function (require) { var $chat_button = self.$el.find('.o_employee_chat_btn'); if (self.state.context.uid === self.state.data.user_id.res_id) { // Hide the button for yourself $chat_button.hide(); - } - else { + } else { $chat_button.off('click').on('click', self._onOpenChat.bind(self)); } }); @@ -34,7 +35,7 @@ odoo.define('hr.employee_chat', function (require) { return this._super(); }, - _onOpenChat: function(ev) { + _onOpenChat: function (ev) { ev.preventDefault(); ev.stopImmediatePropagation(); this.trigger_up('open_chat', { @@ -53,7 +54,11 @@ odoo.define('hr.employee_chat', function (require) { }), _onOpenChat: function (ev) { - this.call('mail_service', 'openDMChatWindow', ev.data.partner_id); + const env = Component.env; + const partner = env.models['mail.partner'].insert({ + id: ev.data.partner_id, + }); + partner.openChat(); }, }); @@ -81,7 +86,11 @@ odoo.define('hr.employee_chat', function (require) { }), _onOpenChat: function (ev) { - this.call('mail_service', 'openDMChatWindow', ev.data.partner_id); + const env = Component.env; + const partner = env.models['mail.partner'].insert({ + id: ev.data.partner_id, + }); + partner.openChat(); }, }); diff --git a/addons/hr/static/tests/many2one_avatar_employee_tests.js b/addons/hr/static/tests/many2one_avatar_employee_tests.js index 3130611d1dc..4a53397fbf7 100644 --- a/addons/hr/static/tests/many2one_avatar_employee_tests.js +++ b/addons/hr/static/tests/many2one_avatar_employee_tests.js @@ -1,6 +1,8 @@ odoo.define('hr.Many2OneAvatarEmployeeTests', function (require) { "use strict"; +const { start } = require('mail/static/src/utils/test_utils.js'); + const FormView = require('web.FormView'); const KanbanView = require('web.KanbanView'); const ListView = require('web.ListView'); @@ -65,9 +67,10 @@ QUnit.module('hr', {}, function () { }); QUnit.test('many2one_avatar_employee widget in list view', async function (assert) { - assert.expect(7); + assert.expect(4); - const list = await createView({ + const { widget: list } = await start({ + hasView: true, View: ListView, model: 'foo', data: this.data, @@ -80,12 +83,6 @@ QUnit.module('hr', {}, function () { }, }); - mock.intercept(list, 'call_service', ev => { - if (ev.data.service === 'mail_service') { - assert.step(`call service ${ev.data.method} ${ev.data.args[0]}`); - } - }, true); - assert.strictEqual(list.$('.o_data_cell span').text(), 'MarioLuigiMarioYoshi'); await dom.click(list.$('.o_data_cell:nth(0) .o_m2o_avatar')); @@ -95,10 +92,10 @@ QUnit.module('hr', {}, function () { assert.verifySteps([ 'read hr.employee 11', - 'call service openDMChatWindow 1', + // 'call service openDMChatWindow 1', 'read hr.employee 7', - 'call service openDMChatWindow 2', - 'call service openDMChatWindow 1', + // 'call service openDMChatWindow 2', + // 'call service openDMChatWindow 1', ]); list.destroy(); @@ -107,7 +104,8 @@ QUnit.module('hr', {}, function () { QUnit.test('many2one_avatar_employee widget in kanban view', async function (assert) { assert.expect(6); - const kanban = await createView({ + const { widget: kanban } = await start({ + hasView: true, View: KanbanView, model: 'foo', data: this.data, @@ -137,7 +135,8 @@ QUnit.module('hr', {}, function () { assert.expect(5); this.data['hr.employee'].records[0].user_partner_id = false; - const form = await createView({ + const { widget: form } = await start({ + hasView: true, View: FormView, model: 'foo', data: this.data, @@ -152,9 +151,6 @@ QUnit.module('hr', {}, function () { }); mock.intercept(form, 'call_service', (ev) => { - if (ev.data.service === 'mail_service') { - throw new Error('should not call mail_service'); - } if (ev.data.service === 'notification') { assert.step(`display notification "${ev.data.args[0].message}"`); } @@ -176,7 +172,8 @@ QUnit.module('hr', {}, function () { QUnit.test('many2one_avatar_employee: click on self', async function (assert) { assert.expect(5); - const form = await createView({ + const { widget: form } = await start({ + hasView: true, View: FormView, model: 'foo', data: this.data, @@ -194,9 +191,6 @@ QUnit.module('hr', {}, function () { }); mock.intercept(form, 'call_service', (ev) => { - if (ev.data.service === 'mail_service') { - throw new Error('should not call mail_service'); - } if (ev.data.service === 'notification') { assert.step(`display notification "${ev.data.args[0].message}"`); } diff --git a/addons/hr/views/hr_department_views.xml b/addons/hr/views/hr_department_views.xml index a3816851d09..480dd06f263 100644 --- a/addons/hr/views/hr_department_views.xml +++ b/addons/hr/views/hr_department_views.xml @@ -17,8 +17,8 @@
- - + +
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index 0f10300a53d..5bc65c16b17 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -189,9 +189,9 @@
- - - + + +
diff --git a/addons/hr/views/hr_job_views.xml b/addons/hr/views/hr_job_views.xml index 3e9a62fc027..46c3a31c974 100644 --- a/addons/hr/views/hr_job_views.xml +++ b/addons/hr/views/hr_job_views.xml @@ -33,7 +33,7 @@
- +
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index b735d30db45..63a53b6553e 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -214,9 +214,9 @@
- - - + + +
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index f7ccdb7b448..a0a491a63d5 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -132,9 +132,9 @@
- - - + + +
@@ -559,9 +559,9 @@
- - - + + +
diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index 167ccba5d6c..1891641fefc 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -55,6 +55,10 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p 'data/hr_holidays_demo.xml', ], 'qweb': [ + 'static/src/bugfix/bugfix.xml', + 'static/src/components/partner_im_status_icon/partner_im_status_icon.xml', + 'static/src/components/thread_icon/thread_icon.xml', + 'static/src/components/thread_viewer/thread_viewer.xml', 'static/src/xml/*.xml', ], 'installable': True, diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.js b/addons/hr_holidays/static/src/bugfix/bugfix.js new file mode 100644 index 00000000000..1f0782785eb --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.js @@ -0,0 +1,10 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('hr_holidays/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.scss b/addons/hr_holidays/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000000..c4272e527b4 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.xml b/addons/hr_holidays/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000000..c17906f75bb --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/addons/hr_holidays/static/src/bugfix/bugfix_tests.js b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000000..a18e79a4020 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('hr_holidays/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('hr_holidays', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss new file mode 100644 index 00000000000..e981e30f4b4 --- /dev/null +++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss @@ -0,0 +1,12 @@ +// ----------------------------------------------------------------------------- +// Style +// ----------------------------------------------------------------------------- + +.o_PartnerImStatusIcon_icon { + &.o-leave-online { + color: $o-enterprise-primary-color; + } + &.o-leave-offline { + color: theme-color('warning'); + } +} diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml new file mode 100644 index 00000000000..c07377d390a --- /dev/null +++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss new file mode 100644 index 00000000000..4caabb3b222 --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_ThreadIcon_leaveOnline { + color: $o-enterprise-primary-color; +} + +.o_ThreadIcon_leaveOffline { + color: theme-color('warning'); +} diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml new file mode 100644 index 00000000000..f8a728a9c6d --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml @@ -0,0 +1,13 @@ + + + + + +
+ + +
+ + + + diff --git a/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.js b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.js new file mode 100644 index 00000000000..01fd38912a1 --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.js @@ -0,0 +1,58 @@ +odoo.define('hr_holidays/static/src/components/thread_viewer/thread_viewer.js', function (require) { +'use strict'; + +const components = { + ThreadViewer: require('mail/static/src/components/thread_viewer/thread_viewer.js'), +}; + +const { str_to_datetime } = require('web.time'); +const { patch } = require('web.utils'); + +patch(components.ThreadViewer, 'hr_holidays/static/src/components/thread_viewer/thread_viewer.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the "out of office" text for the correspondent of the thread if + * applicable. + * + * @returns {string} + */ + getOutOfOfficeText() { + if (!this.threadViewer.thread.correspondent) { + return ""; + } + if (!this.threadViewer.thread.correspondent.out_of_office_date_end) { + return ""; + } + const currentDate = new Date(); + const date = str_to_datetime(this.threadViewer.thread.correspondent.out_of_office_date_end); + const options = { day: 'numeric', month: 'short' }; + if (currentDate.getFullYear() !== date.getFullYear()) { + options.year = 'numeric'; + } + const formattedDate = date.toLocaleDateString(window.navigator.language, options); + return _.str.sprintf(this.env._t("Out of office until %s."), formattedDate); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _useStoreSelector(props) { + const res = this._super(...arguments); + const threadViewer = this.env.models['mail.thread_viewer'].get(props.threadViewerLocalId); + const thread = threadViewer ? threadViewer.thread : undefined; + const correspondent = thread ? thread.correspondent : undefined; + return Object.assign({}, res, { + correspondent: correspondent ? correspondent.__state : undefined, + }); + }, +}); + +}); diff --git a/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.scss b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.scss new file mode 100644 index 00000000000..746690256ff --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.scss @@ -0,0 +1,8 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +.o_ThreadViewer_outOfOffice { + margin-top: 0; + margin-bottom: 0; +} diff --git a/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.xml b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.xml new file mode 100644 index 00000000000..b670d5d8c61 --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_viewer/thread_viewer.xml @@ -0,0 +1,10 @@ + + + + + +
- - - + + +
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index 8be3fa10cf1..3fd0baab43d 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -150,9 +150,9 @@
- - - + + +
diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 29b4ec7b371..eab86ec678b 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -39,7 +39,11 @@ Help your customers with this chat, and analyse their feedback. 'data/mail_shortcode_demo.xml', ], 'depends': ["mail", "rating", "digest"], - 'qweb': ['static/src/xml/*.xml'], + 'qweb': [ + 'static/src/bugfix/bugfix.xml', + 'static/src/components/discuss_sidebar/discuss_sidebar.xml', + 'static/src/components/thread_icon/thread_icon.xml', + ], 'installable': True, 'auto_install': False, 'application': True, diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 99d4a16d60a..89885c84739 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -33,10 +33,7 @@ class LivechatController(http.Controller): def load_templates(self, **kwargs): base_url = request.httprequest.base_url templates = [ - 'mail/static/src/xml/abstract_thread_window.xml', - 'mail/static/src/xml/discuss.xml', - 'mail/static/src/xml/thread.xml', - 'im_livechat/static/src/xml/im_livechat.xml', + 'im_livechat/static/src/legacy/public_livechat.xml', ] return [tools.file_open(tmpl, 'rb').read() for tmpl in templates] @@ -149,7 +146,7 @@ class LivechatController(http.Controller): """ Channel = request.env['mail.channel'] channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1) - channel.notify_typing(is_typing=is_typing, is_website_user=True) + channel.notify_typing(is_typing=is_typing) @http.route('/im_livechat/email_livechat_transcript', type='json', auth='public', cors="*") def email_livechat_transcript(self, uuid, email): diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index acadce91521..a1110b30c4b 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -86,7 +86,7 @@ class MailChannel(models.Model): res = channel.livechat_operator_id.with_context(im_livechat_use_username=True).name_get()[0] channel_infos_dict[channel.id]['operator_pid'] = (res[0], res[1].replace(',', '')) # add the anonymous or partner name - channel_infos_dict[channel.id]['correspondent_name'] = channel._channel_get_livechat_partner_name() + channel_infos_dict[channel.id]['livechat_visitor'] = channel._channel_get_livechat_visitor_info() last_msg = self.env['mail.message'].search([("channel_ids", "in", [channel.id])], limit=1) if last_msg: channel_infos_dict[channel.id]['last_message_date'] = last_msg.date @@ -99,6 +99,26 @@ class MailChannel(models.Model): values['channel_livechat'] = self.search([('channel_type', '=', 'livechat'), ('id', 'in', pinned_channels.ids)]).channel_info() return values + def _channel_get_livechat_visitor_info(self): + partners = self.channel_partner_ids - self.livechat_operator_id + if partners: + return { + 'country': partners[0].country_id.name_get() if partners[0].country_id else False, + 'id': partners[0].id, + 'name': partners[0].name, + } + if self.anonymous_name: + return { + 'country': False, + 'id': False, + 'name': self.anonymous_name, + } + return { + 'country': False, + 'id': False, + 'name': _("Visitor"), + } + def _channel_get_livechat_partner_name(self): if self.livechat_operator_id in self.channel_partner_ids: partners = self.channel_partner_ids - self.livechat_operator_id diff --git a/addons/im_livechat/static/src/bugfix/bugfix.js b/addons/im_livechat/static/src/bugfix/bugfix.js new file mode 100644 index 00000000000..8e0a4170e09 --- /dev/null +++ b/addons/im_livechat/static/src/bugfix/bugfix.js @@ -0,0 +1,10 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('im_livechat/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); diff --git a/addons/im_livechat/static/src/bugfix/bugfix.scss b/addons/im_livechat/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000000..c4272e527b4 --- /dev/null +++ b/addons/im_livechat/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/im_livechat/static/src/bugfix/bugfix.xml b/addons/im_livechat/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000000..c17906f75bb --- /dev/null +++ b/addons/im_livechat/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/addons/im_livechat/static/src/bugfix/bugfix_tests.js b/addons/im_livechat/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000000..33cfeacf3fc --- /dev/null +++ b/addons/im_livechat/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('im_livechat/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('im_livechat', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/im_livechat/static/src/components/discuss/discuss.js b/addons/im_livechat/static/src/components/discuss/discuss.js new file mode 100644 index 00000000000..5b3c266430a --- /dev/null +++ b/addons/im_livechat/static/src/components/discuss/discuss.js @@ -0,0 +1,29 @@ +odoo.define('im_livechat/static/src/components/discuss/discuss.js', function (require) { +'use strict'; + +const components = { + Discuss: require('mail/static/src/components/discuss/discuss.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.Discuss, 'im_livechat/static/src/components/discuss/discuss.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + mobileNavbarTabs(...args) { + return [...this._super(...args), { + icon: 'fa fa-comments', + id: 'livechat', + label: this.env._t("Livechat"), + }]; + } + +}); + +}); diff --git a/addons/im_livechat/static/src/components/discuss/discuss_tests.js b/addons/im_livechat/static/src/components/discuss/discuss_tests.js new file mode 100644 index 00000000000..dc65fc8d4d7 --- /dev/null +++ b/addons/im_livechat/static/src/components/discuss/discuss_tests.js @@ -0,0 +1,93 @@ +odoo.define('im_livechat/static/src/components/discuss/discuss_tests.js', function (require) { +'use strict'; + +const { + afterEach: utilsAfterEach, + beforeEach: utilsBeforeEach, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('im_livechat', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('discuss', {}, function () { +QUnit.module('discuss_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + this.start = async params => { + if (this.widget) { + this.widget.destroy(); + } + let { env, widget } = await utilsStart(Object.assign({}, params, { + autoOpenDiscuss: true, + data: this.data, + hasDiscuss: true, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + utilsAfterEach(this); + if (this.widget) { + this.widget.destroy(); + } + }, +}); + +QUnit.test('livechat in the sidebar', async function (assert) { + assert.expect(5); + + this.data.initMessaging = { + channel_slots: { + channel_livechat: [{ + channel_type: "livechat", + id: 1, + livechat_visitor: { + country: false, + id: false, + name: "Visitor", + }, + }], + }, + }; + + await this.start(); + + assert.containsOnce(document.body, '.o_Discuss_sidebar', + "should have a sidebar section" + ); + const groupLivechat = document.querySelector('.o_DiscussSidebar_groupLivechat'); + assert.ok(groupLivechat, + "should have a channel group livechat" + ); + const grouptitle = groupLivechat.querySelector('.o_DiscussSidebar_groupTitle'); + assert.strictEqual( + grouptitle.textContent.trim(), + "Livechat", + "should have a channel group named 'Livechat'" + ); + const livechat = groupLivechat.querySelector(` + .o_DiscussSidebarItem[data-thread-local-id="${ + this.env.models['mail.thread'].find(thread => + thread.id === 1 && + thread.model === 'mail.channel' + ).localId + }"] + `); + assert.ok( + livechat, + "should have a livechat in sidebar" + ); + assert.strictEqual( + livechat.textContent, + "Visitor", + "should have 'Visitor' as livechat name" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js new file mode 100644 index 00000000000..36656c877c7 --- /dev/null +++ b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js @@ -0,0 +1,70 @@ +odoo.define('im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', function (require) { +'use strict'; + +const components = { + DiscussSidebar: require('mail/static/src/components/discuss_sidebar/discuss_sidebar.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Return the list of livechats that match the quick search value input. + * + * @returns {mail.thread[]} + */ + quickSearchOrderedAndPinnedLivechatList() { + const allOrderedAndPinnedLivechats = this.env.models['mail.thread'].all(thread => + thread.channel_type === 'livechat' && + thread.isPinned && + thread.model === 'mail.channel' + ); + if (!this.discuss.sidebarQuickSearchValue) { + return allOrderedAndPinnedLivechats; + } + const qsVal = this.discuss.sidebarQuickSearchValue.toLowerCase(); + return allOrderedAndPinnedLivechats.filter(livechat => { + const nameVal = livechat.displayName.toLowerCase(); + return nameVal.includes(qsVal); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _useStoreCompareDepth() { + return Object.assign(this._super(...arguments), { + allOrderedAndPinnedLivechats: 1, + }); + }, + /** + * Override to include livechat channels on the sidebar. + * + * @override + */ + _useStoreSelector(props) { + return Object.assign(this._super(...arguments), { + allOrderedAndPinnedLivechats: this.env.models['mail.thread'] + .all(thread => + thread.channel_type === 'livechat' && + thread.isPinned && + thread.model === 'mail.channel' + ) + .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1) + .map(livechat => livechat.__state), + } + ); + }, + +}); + +}); diff --git a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml new file mode 100644 index 00000000000..15c6fc35e4d --- /dev/null +++ b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.xml @@ -0,0 +1,25 @@ + + + + + + +
+
+
+ Livechat +
+
+
+ + + +
+
+
+
+
+
diff --git a/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js new file mode 100644 index 00000000000..b2273d9989b --- /dev/null +++ b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js @@ -0,0 +1,26 @@ +odoo.define('im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', function (require) { +'use strict'; + +const components = { + DiscussSidebarItem: require('mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.DiscussSidebarItem, 'im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + hasUnpin(...args) { + const res = this._super(...args); + return res || this.thread.channel_type === 'livechat'; + } + +}); + +}); diff --git a/addons/im_livechat/static/src/components/notification_list/notification_list.js b/addons/im_livechat/static/src/components/notification_list/notification_list.js new file mode 100644 index 00000000000..c0ab76b28f3 --- /dev/null +++ b/addons/im_livechat/static/src/components/notification_list/notification_list.js @@ -0,0 +1,36 @@ +odoo.define('im_livechat/static/src/components/notification_list/notification_list.js', function (require) { +'use strict'; + +const components = { + NotificationList: require('mail/static/src/components/notification_list/notification_list.js'), +}; + +const { patch } = require('web.utils'); + +components.NotificationList._allowedFilters.push('livechat'); + +patch(components.NotificationList, 'im_livechat/static/src/components/notification_list/notification_list.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Override to include livechat channels. + * + * @override + */ + _useStoreSelectorThreads(props) { + if (props.filter === 'livechat') { + return this.env.models['mail.thread'].all(thread => + thread.channel_type === 'livechat' && + thread.isPinned && + thread.model === 'mail.channel' + ); + } + return this._super(...arguments); + }, + +}); + +}); diff --git a/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml b/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml new file mode 100644 index 00000000000..81c93868e14 --- /dev/null +++ b/addons/im_livechat/static/src/components/thread_icon/thread_icon.xml @@ -0,0 +1,19 @@ + + + + + + + + + +
+ + + + + diff --git a/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js b/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js new file mode 100644 index 00000000000..9edd905eabb --- /dev/null +++ b/addons/im_livechat/static/src/components/thread_icon/thread_icon_tests.js @@ -0,0 +1,128 @@ +odoo.define('im_livechat/static/src/components/thread_icon/thread_icon_tests.js', function (require) { +'use strict'; + +const components = { + ThreadIcon: require('mail/static/src/components/thread_icon/thread_icon.js'), +}; +const { + afterEach: utilsAfterEach, + afterNextRender, + beforeEach: utilsBeforeEach, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('im_livechat', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('thread_icon', {}, function () { +QUnit.module('thread_icon_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + this.createThreadIcon = async thread => { + const ThreadIconComponent = components.ThreadIcon; + ThreadIconComponent.env = this.env; + this.component = new ThreadIconComponent(null, { threadLocalId: thread.localId }); + await this.component.mount(this.widget.el); + }; + + this.start = async params => { + let { env, widget } = await utilsStart(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + + }, + afterEach() { + utilsAfterEach(this); + if (this.component) { + this.component.destroy(); + } + if (this.widget) { + this.widget.destroy(); + } + this.env = undefined; + delete components.ThreadIcon.env; + }, +}); + +QUnit.test('livechat: public website visitor is typing', async function (assert) { + assert.expect(4); + + Object.assign(this.data.initMessaging, { + channel_slots: { + channel_livechat: [{ + channel_type: 'livechat', + id: 20, + livechat_visitor: { + country: false, + id: false, + name: "Visitor", + }, + members: [{ + email: 'admin@odoo.com', + id: 3, + name: 'Admin', + }], + }], + }, + public_partner: { + active: false, + display_name: "Public Partner", + id: 7, + }, + }); + await this.start({ + env: { + session: { + name: 'Admin', + partner_display_name: 'Your Company, Admin', + partner_id: 3, + uid: 2, + }, + }, + }); + const thread = this.env.models['mail.thread'].find(thread => + thread.id === 20 && + thread.model === 'mail.channel' + ); + await this.createThreadIcon(thread); + assert.containsOnce( + document.body, + '.o_ThreadIcon', + "should have thread icon" + ); + assert.containsOnce( + document.body, + '.o_ThreadIcon .fa.fa-comments', + "should have default livechat icon" + ); + + // simulate receive typing notification from website visitor "is typing" + await afterNextRender(() => { + const typingData = { + info: 'typing_status', + partner_id: 7, // public partner_id + is_typing: true, + }; + const notification = [[false, 'mail.channel', 20], typingData]; + this.widget.call('bus_service', 'trigger', 'notification', [notification]); + }); + assert.containsOnce( + document.body, + '.o_ThreadIcon_typing', + "should have thread icon with visitor currently typing" + ); + assert.strictEqual( + document.querySelector('.o_ThreadIcon_typing').title, + "Visitor is typing...", + "title of icon should tell visitor is currently typing" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js new file mode 100644 index 00000000000..3cd95824634 --- /dev/null +++ b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js @@ -0,0 +1,28 @@ +odoo.define('im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js', function (require) { +'use strict'; + +const components = { + ThreadNeedactionPreview: require('mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.ThreadNeedactionPreview, 'thread_needaction_preview', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + image(...args) { + if (this.thread.channel_type === 'livechat') { + return '/mail/static/src/img/smiley/avatar.jpg'; + } + return this._super(...args); + } + +}); + +}); diff --git a/addons/im_livechat/static/src/components/thread_preview/thread_preview.js b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js new file mode 100644 index 00000000000..ea752b56936 --- /dev/null +++ b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js @@ -0,0 +1,28 @@ +odoo.define('im_livechat/static/src/components/thread_preview/thread_preview.js', function (require) { +'use strict'; + +const components = { + ThreadPreview: require('mail/static/src/components/thread_preview/thread_preview.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.ThreadPreview, 'im_livechat/static/src/components/thread_preview/thread_preview.js', { + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + image(...args) { + if (this.thread.channel_type === 'livechat') { + return '/mail/static/src/img/smiley/avatar.jpg'; + } + return this._super(...args); + } + +}); + +}); diff --git a/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js b/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js new file mode 100644 index 00000000000..0fdae483eea --- /dev/null +++ b/addons/im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js @@ -0,0 +1,123 @@ +odoo.define('im_livechat/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js', function (require) { +'use strict'; + +const components = { + ThreadTextualTypingStatus: require('mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js'), +}; +const { + afterEach: utilsAfterEach, + afterNextRender, + beforeEach: utilsBeforeEach, + start: utilsStart, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('im_livechat', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('thread_textual_typing_status', {}, function () { +QUnit.module('thread_textual_typing_status_tests.js', { + beforeEach() { + utilsBeforeEach(this); + + this.createThreadTextualTypingStatusComponent = async thread => { + const ThreadTextualTypingStatusComponent = components.ThreadTextualTypingStatus; + ThreadTextualTypingStatusComponent.env = this.env; + this.component = new ThreadTextualTypingStatusComponent(null, { + threadLocalId: thread.localId, + }); + await this.component.mount(this.widget.el); + }; + + this.start = async params => { + if (this.widget) { + this.widget.destroy(); + } + let { env, widget } = await utilsStart(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + async afterEach() { + utilsAfterEach(this); + if (this.component) { + this.component.destroy(); + } + if (this.widget) { + this.widget.destroy(); + } + this.env = undefined; + delete components.ThreadTextualTypingStatus.env; + }, +}); + +QUnit.test('receive visitor typing status "is typing"', async function (assert) { + assert.expect(2); + + Object.assign(this.data.initMessaging, { + channel_slots: { + channel_livechat: [{ + channel_type: 'livechat', + id: 20, + livechat_visitor: { + country: false, + id: false, + name: "Visitor", + }, + members: [{ + email: 'admin@odoo.com', + id: 3, + name: 'Admin', + }], + }], + }, + public_partner: { + active: false, + display_name: "Public Partner", + id: 7, + }, + }); + await this.start({ + env: { + session: { + name: 'Admin', + partner_display_name: 'Your Company, Admin', + partner_id: 3, + uid: 2, + }, + }, + }); + const thread = this.env.models['mail.thread'].find(thread => + thread.id === 20 && + thread.model === 'mail.channel' + ); + await this.createThreadTextualTypingStatusComponent(thread); + + assert.strictEqual( + document.querySelector('.o_ThreadTextualTypingStatus').textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from visitor "is typing" + await afterNextRender(() => { + const typingData = { + info: 'typing_status', + partner_id: 7, // public partner_id + is_typing: true, + }; + const notification = [[false, 'mail.channel', 20], typingData]; + this.widget.call('bus_service', 'trigger', 'notification', [notification]); + }); + assert.strictEqual( + document.querySelector('.o_ThreadTextualTypingStatus').textContent, + "Visitor is typing...", + "Should display that visitor is typing" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js deleted file mode 100644 index 0296417bb10..00000000000 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ /dev/null @@ -1,576 +0,0 @@ -odoo.define('im_livechat.im_livechat', function (require) { -"use strict"; - -require('bus.BusService'); -var concurrency = require('web.concurrency'); -var config = require('web.config'); -var core = require('web.core'); -var session = require('web.session'); -var time = require('web.time'); -var utils = require('web.utils'); -var Widget = require('web.Widget'); - -var WebsiteLivechat = require('im_livechat.model.WebsiteLivechat'); -var WebsiteLivechatMessage = require('im_livechat.model.WebsiteLivechatMessage'); -var WebsiteLivechatWindow = require('im_livechat.WebsiteLivechatWindow'); - -var _t = core._t; -var QWeb = core.qweb; - -// Constants -var LIVECHAT_COOKIE_HISTORY = 'im_livechat_history'; -var HISTORY_LIMIT = 15; - -var RATING_TO_EMOJI = { - "10":"😊", - "5":"😐", - "1":"😞" -}; - -// History tracking -var page = window.location.href.replace(/^.*\/\/[^/]+/, ''); -var pageHistory = utils.get_cookie(LIVECHAT_COOKIE_HISTORY); -var urlHistory = []; -if (pageHistory) { - urlHistory = JSON.parse(pageHistory) || []; -} -if (!_.contains(urlHistory, page)) { - urlHistory.push(page); - while (urlHistory.length > HISTORY_LIMIT) { - urlHistory.shift(); - } - utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(urlHistory), 60*60*24); // 1 day cookie -} - -var LivechatButton = Widget.extend({ - className:'openerp o_livechat_button d-print-none', - custom_events: { - 'close_chat_window': '_onCloseChatWindow', - 'post_message_chat_window': '_onPostMessageChatWindow', - 'save_chat_window': '_onSaveChatWindow', - 'updated_typing_partners': '_onUpdatedTypingPartners', - 'updated_unread_counter': '_onUpdatedUnreadCounter', - }, - events: { - 'click': '_openChat' - }, - init: function (parent, serverURL, options) { - this._super(parent); - this.options = _.defaults(options || {}, { - input_placeholder: _t("Ask something ..."), - default_username: _t("Visitor"), - button_text: _t("Chat with one of our collaborators"), - default_message: _t("How may I help you?"), - }); - - this._history = null; - // livechat model - this._livechat = null; - // livechat window - this._chatWindow = null; - this._messages = []; - this._serverURL = serverURL; - }, - willStart: function () { - var self = this; - var cookie = utils.get_cookie('im_livechat_session'); - var ready; - if (!cookie) { - ready = session.rpc('/im_livechat/init', {channel_id: this.options.channel_id}) - .then(function (result) { - if (!result.available_for_me) { - return Promise.reject(); - } - self._rule = result.rule; - }); - } else { - var channel = JSON.parse(cookie); - ready = session.rpc('/mail/chat_history', {uuid: channel.uuid, limit: 100}) - .then(function (history) { - self._history = history; - }); - } - return ready.then(this._loadQWebTemplate.bind(this)); - }, - start: function () { - this.$el.text(this.options.button_text); - if (this._history) { - _.each(this._history.reverse(), this._addMessage.bind(this)); - this._openChat(); - } else if (!config.device.isMobile && this._rule.action === 'auto_popup') { - var autoPopupCookie = utils.get_cookie('im_livechat_auto_popup'); - if (!autoPopupCookie || JSON.parse(autoPopupCookie)) { - this._autoPopupTimeout = - setTimeout(this._openChat.bind(this), this._rule.auto_popup_timer*1000); - } - } - this.call('bus_service', 'onNotification', this, this._onNotification); - if (this.options.button_background_color) { - this.$el.css('background-color', this.options.button_background_color); - } - if (this.options.button_text_color) { - this.$el.css('color', this.options.button_text_color); - } - return this._super(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - - /** - * @private - * @param {Object} data - * @param {Object} [options={}] - */ - _addMessage: function (data, options) { - options = _.extend({}, this.options, options, { - serverURL: this._serverURL, - }); - var message = new WebsiteLivechatMessage(this, data, options); - - var hasAlreadyMessage = _.some(this._messages, function (msg) { - return message.getID() === msg.getID(); - }); - if (hasAlreadyMessage) { - return; - } - - if (this._livechat) { - this._livechat.addMessage(message); - } - - if (options && options.prepend) { - this._messages.unshift(message); - } else { - this._messages.push(message); - } - }, - /** - * @private - */ - _askFeedback: function () { - this._chatWindow.$('.o_thread_composer input').prop('disabled', true); - - var feedback = new Feedback(this, this._livechat); - this._chatWindow.replaceContentWith(feedback); - - feedback.on('send_message', this, this._sendMessage); - feedback.on('feedback_sent', this, this._closeChat); - }, - /** - * @private - */ - _closeChat: function () { - this._chatWindow.destroy(); - utils.set_cookie('im_livechat_session', "", -1); // remove cookie - }, - /** - * @private - * @param {Array} notification - */ - _handleNotification: function (notification){ - if (this._livechat && (notification[0] === this._livechat.getUUID())) { - if (notification[1]._type === 'history_command') { // history request - var cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY); - var history = cookie ? JSON.parse(cookie) : []; - session.rpc('/im_livechat/history', { - pid: this._livechat.getOperatorPID()[0], - channel_uuid: this._livechat.getUUID(), - page_history: history, - }); - } else if (notification[1].info === 'typing_status') { - var isWebsiteUser = notification[1].is_website_user; - if (isWebsiteUser) { - return; // do not handle typing status notification of myself - } - var partnerID = notification[1].partner_id; - if (notification[1].is_typing) { - this._livechat.registerTyping({ partnerID: partnerID }); - } else { - this._livechat.unregisterTyping({ partnerID: partnerID }); - } - } else { // normal message - // If message from notif is already in chatter messages, stop handling - if (this._messages.some(message => message.getID() === notification[1].id)) { - this._livechat.unregisterTyping({ partnerID: notification[1].author_id[0] }); - return; - } - this._addMessage(notification[1]); - this._renderMessages(); - if (this._chatWindow.isFolded() || !this._chatWindow.isAtBottom()) { - this._livechat.incrementUnreadCounter(); - } - } - } - }, - /** - * @private - */ - _loadQWebTemplate: function () { - return session.rpc('/im_livechat/load_templates').then(function (templates) { - _.each(templates, function (template) { - QWeb.add_template(template); - }); - }); - }, - /** - * @private - */ - _openChat: _.debounce(function () { - if (this._openingChat) { - return; - } - var self = this; - var cookie = utils.get_cookie('im_livechat_session'); - var def; - this._openingChat = true; - clearTimeout(this._autoPopupTimeout); - if (cookie) { - def = Promise.resolve(JSON.parse(cookie)); - } else { - this._messages = []; // re-initialize messages cache - def = session.rpc('/im_livechat/get_session', { - channel_id : this.options.channel_id, - anonymous_name : this.options.default_username, - previous_operator_id: this._get_previous_operator_id(), - }, {shadow: true}); - } - def.then(function (livechatData) { - if (!livechatData || !livechatData.operator_pid) { - self.displayNotification({ - message: _t("No available collaborator, please try again later."), - sticky: true - }); - } else { - self._livechat = new WebsiteLivechat({ - parent: self, - data: livechatData - }); - return self._openChatWindow().then(function () { - if (!self._history) { - self._sendWelcomeMessage(); - } - self._renderMessages(); - self.call('bus_service', 'addChannel', self._livechat.getUUID()); - self.call('bus_service', 'startPolling'); - - utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60*60); - utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60); - if (livechatData.operator_pid[0]) { - // livechatData.operator_pid contains a tuple (id, name) - // we are only interested in the id - var operatorPidId = livechatData.operator_pid[0]; - var oneWeek = 7*24*60*60; - utils.set_cookie('im_livechat_previous_operator_pid', operatorPidId, oneWeek); - } - }); - } - }).then(function () { - self._openingChat = false; - }).guardedCatch(function() { - self._openingChat = false; - }); - }, 200, true), - /** - * Will try to get a previous operator for this visitor. - * If the visitor already had visitor A, it's better for his user experience - * to get operator A again. - * - * The information is stored in the 'im_livechat_previous_operator_pid' cookie. - * - * @private - * @return {integer} operator_id.partner_id.id if the cookie is set - */ - _get_previous_operator_id: function () { - var cookie = utils.get_cookie('im_livechat_previous_operator_pid'); - if (cookie) { - return cookie; - } - - return null; - }, - /** - * @private - * @return {Promise} - */ - _openChatWindow: function () { - var self = this; - var options = { - displayStars: false, - headerBackgroundColor: this.options.header_background_color, - placeholder: this.options.input_placeholder || "", - titleColor: this.options.title_color, - }; - this._chatWindow = new WebsiteLivechatWindow(this, this._livechat, options); - return this._chatWindow.appendTo($('body')).then(function () { - var cssProps = {bottom: 0}; - cssProps[_t.database.parameters.direction === 'rtl' ? 'left' : 'right'] = 0; - self._chatWindow.$el.css(cssProps); - self.$el.hide(); - }); - }, - /** - * @private - */ - _renderMessages: function () { - var shouldScroll = !this._chatWindow.isFolded() && this._chatWindow.isAtBottom(); - this._livechat.setMessages(this._messages); - this._chatWindow.render(); - if (shouldScroll) { - this._chatWindow.scrollToBottom(); - } - }, - /** - * @private - * @param {Object} message - * @return {Promise} - */ - _sendMessage: function (message) { - var self = this; - return session - .rpc('/mail/chat_post', {uuid: this._livechat.getUUID(), message_content: message.content}) - .then(function (messageId) { - if (!messageId) { - self.displayNotification({ - message: _t("Session expired... Please refresh and try again."), - sticky: true - }); - self._closeChat(); - } - self._chatWindow.scrollToBottom(); - }); - }, - /** - * @private - */ - _sendWelcomeMessage: function () { - if (this.options.default_message) { - this._addMessage({ - id: '_welcome', - attachment_ids: [], - author_id: this._livechat.getOperatorPID(), - body: this.options.default_message, - channel_ids: [this._livechat.getID()], - date: time.datetime_to_str(new Date()), - tracking_value_ids: [], - }, {prepend: true}); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - * @param {OdooEvent} ev - */ - _onCloseChatWindow: function (ev) { - ev.stopPropagation(); - var isComposerDisabled = this._chatWindow.$('.o_thread_composer input').prop('disabled'); - var shouldAskFeedback = !isComposerDisabled && _.find(this._messages, function (message) { - return message.getID() !== '_welcome'; - }); - if (shouldAskFeedback) { - this._chatWindow.toggleFold(false); - this._askFeedback(); - } else { - this._closeChat(); - } - }, - /** - * @private - * @param {Array[]} notifications - */ - _onNotification: function (notifications) { - var self = this; - _.each(notifications, function (notification) { - self._handleNotification(notification); - }); - }, - /** - * @private - * @param {OdooEvent} ev - * @param {Object} ev.data.messageData - */ - _onPostMessageChatWindow: function (ev) { - ev.stopPropagation(); - var self = this; - var messageData = ev.data.messageData; - this._sendMessage(messageData).guardedCatch(function (reason) { - reason.event.preventDefault(); - return self._sendMessage(messageData); // try again just in case - }); - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onSaveChatWindow: function (ev) { - ev.stopPropagation(); - utils.set_cookie('im_livechat_session', JSON.stringify(this._livechat.toData()), 60*60); - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onUpdatedTypingPartners: function (ev) { - ev.stopPropagation(); - this._chatWindow.renderHeader(); - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onUpdatedUnreadCounter: function (ev) { - ev.stopPropagation(); - this._chatWindow.renderHeader(); - }, -}); - -/* - * Rating for Livechat - * - * This widget displays the 3 rating smileys, and a textarea to add a reason - * (only for red smiley), and sends the user feedback to the server. - */ -var Feedback = Widget.extend({ - template: 'im_livechat.FeedBack', - - events: { - 'click .o_livechat_rating_choices img': '_onClickSmiley', - 'click .o_livechat_no_feedback span': '_onClickNoFeedback', - 'click .o_rating_submit_button': '_onClickSend', - 'click .o_email_chat_button': '_onEmailChat', - 'click .o_livechat_email_error .alert-link': '_onTryAgain', - }, - - /** - * @param {?} parent - * @param {im_livechat.model.WebsiteLivechat} livechat - */ - init: function (parent, livechat) { - this._super(parent); - this._livechat = livechat; - this.server_origin = session.origin; - this.rating = undefined; - this.dp = new concurrency.DropPrevious(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @param {Object} options - */ - _sendFeedback: function (reason) { - var self = this; - var args = { - uuid: this._livechat.getUUID(), - rate: this.rating, - reason: reason, - }; - this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () { - var emoji = RATING_TO_EMOJI[self.rating] || "??" ; - var content = _.str.sprintf(_t("Rating: %s"), emoji); - if (reason) { - content += " \n" + reason; - } - self.trigger('send_message', { content: content, isFeedback: true }); - }); - }, - /** - * @private - */ - _showThanksMessage: function () { - this.$('.o_livechat_rating_box').empty().append($('
', { - text: _t('Thank you for your feedback'), - class: 'text-muted' - })); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onClickNoFeedback: function () { - this.trigger('feedback_sent'); // will close the chat - }, - /** - * @private - */ - _onClickSend: function () { - this.$('.o_livechat_rating_reason').hide(); - this._showThanksMessage(); - if (_.isNumber(this.rating)) { - this._sendFeedback(this.$('textarea').val()); - } - }, - /** - * @private - * @param {MouseEvent} ev - */ - _onClickSmiley: function (ev) { - this.rating = parseInt($(ev.currentTarget).data('value')); - this.$('.o_livechat_rating_choices img').removeClass('selected'); - this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected'); - - // only display textearea if bad smiley selected - if (this.rating !== 5) { - this.$('.o_livechat_rating_reason').show(); - } else { - this.$('.o_livechat_rating_reason').hide(); - this._showThanksMessage(); - this._sendFeedback(); - } - }, - /** - * @private - */ - _onEmailChat: function () { - var self = this; - var $email = this.$('#o_email'); - - if (utils.is_email($email.val())) { - $email.removeAttr('title').removeClass('is-invalid').prop('disabled', true); - this.$('.o_email_chat_button').prop('disabled', true); - this._rpc({ - route: '/im_livechat/email_livechat_transcript', - params: { - uuid: this._livechat.getUUID(), - email: $email.val(), - } - }).then(function () { - self.$('.o_livechat_email').html($('', { text: _t('Conversation Sent') })); - }).guardedCatch(function () { - self.$('.o_livechat_email').hide(); - self.$('.o_livechat_email_error').show(); - }); - } else { - $email.addClass('is-invalid').prop('title', _t('Invalid email address')); - } - }, - /** - * @private - */ - _onTryAgain: function () { - this.$('#o_email').prop('disabled', false); - this.$('.o_email_chat_button').prop('disabled', false); - this.$('.o_livechat_email_error').hide(); - this.$('.o_livechat_email').show(); - }, -}); - -return { - LivechatButton: LivechatButton, - Feedback: Feedback, -}; - -}); diff --git a/addons/im_livechat/static/src/js/im_livechat_backend.js b/addons/im_livechat/static/src/js/im_livechat_backend.js deleted file mode 100644 index e303f48bf2a..00000000000 --- a/addons/im_livechat/static/src/js/im_livechat_backend.js +++ /dev/null @@ -1,32 +0,0 @@ -odoo.define('im_livechat.Discuss', function (require) { -"use strict"; - -var Discuss = require('mail.Discuss'); - -Discuss.include({ - /** - * Override to sort livechats by last message's date - * - * @override - * @private - * @param {mail.model.Channel[]} channels - * @returns {mail.model.Channel[]} - */ - _sortChannels: function (channels) { - var partition = _.partition(channels, function (channel) { - return channel.getType() === 'livechat'; - }); - partition[0].sort(function (c1, c2) { - if (!c1.hasMessages()) { - return -1; - } else if (!c2.hasMessages()) { - return 1; - } - return c2.getLastMessage().getDate().diff(c1.getLastMessage().getDate()); - }); - channels = partition[0].concat(partition[1]); - return this._super(channels); - }, -}); - -}); diff --git a/addons/im_livechat/static/src/js/models/website_livechat.js b/addons/im_livechat/static/src/js/models/website_livechat.js deleted file mode 100644 index cf5bc36c7b6..00000000000 --- a/addons/im_livechat/static/src/js/models/website_livechat.js +++ /dev/null @@ -1,186 +0,0 @@ -odoo.define('im_livechat.model.WebsiteLivechat', function (require) { -"use strict"; - -var AbstractThread = require('mail.model.AbstractThread'); -var ThreadTypingMixin = require('mail.model.ThreadTypingMixin'); - -var session = require('web.session'); - -/** - * Thread model that represents a livechat on the website-side. This livechat - * is not linked to the mail service. - */ -var WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { - - /** - * @override - * @private - * @param {Object} params - * @param {Object} params.data - * @param {boolean} [params.data.folded] states whether the livechat is - * folded or not. It is considered only if this is defined and it is a - * boolean. - * @param {integer} params.data.id the ID of this livechat. - * @param {integer} [params.data.message_unread_counter] the unread counter - * of this livechat. - * @param {Array} params.data.operator_pid - * @param {string} params.data.name the name of this livechat. - * @param {string} [params.data.state] if 'folded', the livechat is folded. - * This is ignored if `folded` is provided and is a boolean value. - * @param {string} params.data.uuid the UUID of this livechat. - * @param {im_livechat.im_livechat.LivechatButton} params.parent - */ - init: function (params) { - this._super.apply(this, arguments); - ThreadTypingMixin.init.call(this, arguments); - - this._members = []; - this._operatorPID = params.data.operator_pid; - this._uuid = params.data.uuid; - - if (params.data.message_unread_counter !== undefined) { - this._unreadCounter = params.data.message_unread_counter; - } - - if (_.isBoolean(params.data.folded)) { - this._folded = params.data.folded; - } else { - this._folded = params.data.state === 'folded'; - } - - // Necessary for thread typing mixin to display is typing notification - // bar text (at least, for the operator in the members). - this._members.push({ - id: this._operatorPID[0], - name: this._operatorPID[1] - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - * @returns {im_livechat.model.WebsiteLivechatMessage[]} - */ - getMessages: function () { - return this._messages; - }, - /** - * @returns {Array} - */ - getOperatorPID: function () { - return this._operatorPID; - }, - /** - * @returns {string} - */ - getUUID: function () { - return this._uuid; - }, - /** - * Increments the unread counter of this livechat by 1 unit. - * - * Note: this public method makes sense because the management of messages - * for website livechat is external. This method should be dropped when - * this class handles messages by itself. - */ - incrementUnreadCounter: function () { - this._incrementUnreadCounter(); - }, - /** - * AKU: hack for the moment - * - * @param {im_livechat.model.WebsiteLivechatMessage[]} messages - */ - setMessages: function (messages) { - this._messages = messages; - }, - /** - * @returns {Object} - */ - toData: function () { - return { - folded: this.isFolded(), - id: this.getID(), - message_unread_counter: this.getUnreadCounter(), - operator_pid: this.getOperatorPID(), - name: this.getName(), - uuid: this.getUUID(), - }; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {Object} params - * @param {boolean} params.isWebsiteUser - * @returns {boolean} - */ - _isTypingMyselfInfo: function (params) { - return params.isWebsiteUser; - }, - /** - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {Object} params - * @param {boolean} params.typing - * @returns {Promise} - */ - _notifyMyselfTyping: function (params) { - return session.rpc('/im_livechat/notify_typing', { - uuid: this.getUUID(), - is_typing: params.typing, - }, { shadow: true }); - }, - /** - * Warn views that the list of users that are currently typing on this - * livechat has been updated. - * - * @override {mail.model.ThreadTypingMixin} - * @private - */ - _warnUpdatedTypingPartners: function () { - this.trigger_up('updated_typing_partners'); - }, - /** - * Warn that the unread counter has been updated on this livechat - * - * @override - * @private - */ - _warnUpdatedUnreadCounter: function () { - this.trigger_up('updated_unread_counter'); - }, - - //-------------------------------------------------------------------------- - // Handler - //-------------------------------------------------------------------------- - - /** - * Override so that it only unregister typing operators. - * - * Note that in the frontend, there is no way to identify a message that is - * from the current user, because there is no partner ID in the session and - * a message with an author sets the partner ID of the author. - * - * @override {mail.model.ThreadTypingMixin} - * @private - * @param {mail.model.AbstractMessage} message - */ - _onTypingMessageAdded: function (message) { - var operatorID = this.getOperatorPID()[0]; - if (message.hasAuthor() && message.getAuthorID() === operatorID) { - this.unregisterTyping({ partnerID: operatorID }); - } - }, -}); - -return WebsiteLivechat; - -}); diff --git a/addons/im_livechat/static/src/js/models/website_livechat_message.js b/addons/im_livechat/static/src/js/models/website_livechat_message.js deleted file mode 100644 index 17634e11d2f..00000000000 --- a/addons/im_livechat/static/src/js/models/website_livechat_message.js +++ /dev/null @@ -1,66 +0,0 @@ -odoo.define('im_livechat.model.WebsiteLivechatMessage', function (require) { -"use strict"; - -var AbstractMessage = require('mail.model.AbstractMessage'); - -/** - * This is a message that is handled by im_livechat, without making use of the - * mail.Manager. The purpose of this is to make im_livechat compatible with - * mail.widget.Thread. - * - * @see mail.model.AbstractMessage for more information. - */ -var WebsiteLivechatMessage = AbstractMessage.extend({ - - /** - * @param {im_livechat.im_livechat.LivechatButton} parent - * @param {Object} data - * @param {Object} options - * @param {string} options.default_username - * @param {string} options.serverURL - */ - init: function (parent, data, options) { - this._super.apply(this, arguments); - - this._defaultUsername = options.default_username; - this._serverURL = options.serverURL; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Get the relative url of the avatar to display next to the message - * - * @override - * @return {string} - */ - getAvatarSource: function () { - var source = this._serverURL; - if (this.hasAuthor()) { - source += '/web/partner_image/' + this.getAuthorID(); - } else { - source += '/mail/static/src/img/smiley/avatar.jpg'; - } - return source; - }, - /** - * Get the text to display for the author of the message - * - * Rule of precedence for the displayed author:: - * - * author name > default usernane - * - * @override - * @return {string} - */ - getDisplayedAuthor: function () { - return this._super.apply(this, arguments) || this._defaultUsername; - }, - -}); - -return WebsiteLivechatMessage; - -}); diff --git a/addons/im_livechat/static/src/js/website_livechat_window.js b/addons/im_livechat/static/src/js/website_livechat_window.js deleted file mode 100644 index 06c13de3dfb..00000000000 --- a/addons/im_livechat/static/src/js/website_livechat_window.js +++ /dev/null @@ -1,105 +0,0 @@ -odoo.define('im_livechat.WebsiteLivechatWindow', function (require) { -"use strict"; - -var AbstractThreadWindow = require('mail.AbstractThreadWindow'); - -/** - * This is the widget that represent windows of livechat in the frontend. - * - * @see mail.AbstractThreadWindow for more information - */ -var LivechatWindow = AbstractThreadWindow.extend({ - events: _.extend(AbstractThreadWindow.prototype.events, { - 'input .o_composer_text_field': '_onInput', - }), - /** - * @override - * @param {im_livechat.im_livechat.LivechatButton} parent - * @param {im_livechat.model.WebsiteLivechat} thread - * @param {Object} [options={}] - * @param {string} [options.headerBackgroundColor] - * @param {string} [options.titleColor] - */ - init(parent, thread, options={}) { - this._super.apply(this, arguments); - this._thread = thread; - }, - /** - * @override - * @return {Promise} - */ - async start() { - await this._super(...arguments); - if (this.options.headerBackgroundColor) { - this.$('.o_thread_window_header').css('background-color', this.options.headerBackgroundColor); - } - if (this.options.titleColor) { - this.$('.o_thread_window_header').css('color', this.options.titleColor); - } - }, - - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - close: function () { - this.trigger_up('close_chat_window'); - }, - /** - * Replace the thread content with provided new content - * - * @param {$.Element} $element - */ - replaceContentWith: function ($element) { - $element.replace(this._threadWidget.$el); - }, - /** - * Warn the parent widget (LivechatButton) - * - * @override - * @param {boolean} folded - */ - toggleFold: function () { - this._super.apply(this, arguments); - this.trigger_up('save_chat_window'); - this.updateVisualFoldState(); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - * @param {Object} messageData - */ - _postMessage: function (messageData) { - this.trigger_up('post_message_chat_window', { messageData: messageData }); - this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the input in the composer changes - * - * @private - */ - _onInput: function () { - if (this.hasThread() && this._thread.hasTypingNotification()) { - var isTyping = this.$input.val().length > 0; - this._thread.setMyselfTyping({ typing: isTyping }); - } - }, -}); - -return LivechatWindow; - -}); diff --git a/addons/im_livechat/static/src/legacy/public_livechat.js b/addons/im_livechat/static/src/legacy/public_livechat.js new file mode 100644 index 00000000000..1b6d5cda160 --- /dev/null +++ b/addons/im_livechat/static/src/legacy/public_livechat.js @@ -0,0 +1,3861 @@ +odoo.define('im_livechat.legacy.im_livechat.im_livechat', function (require) { +"use strict"; + +require('bus.BusService'); +var concurrency = require('web.concurrency'); +var config = require('web.config'); +var core = require('web.core'); +var session = require('web.session'); +var time = require('web.time'); +var utils = require('web.utils'); +var Widget = require('web.Widget'); + +var WebsiteLivechat = require('im_livechat.legacy.im_livechat.model.WebsiteLivechat'); +var WebsiteLivechatMessage = require('im_livechat.legacy.im_livechat.model.WebsiteLivechatMessage'); +var WebsiteLivechatWindow = require('im_livechat.legacy.im_livechat.WebsiteLivechatWindow'); + +var _t = core._t; +var QWeb = core.qweb; + +// Constants +var LIVECHAT_COOKIE_HISTORY = 'im_livechat_history'; +var HISTORY_LIMIT = 15; + +var RATING_TO_EMOJI = { + "10": "😊", + "5": "😐", + "1": "😞" +}; + +// History tracking +var page = window.location.href.replace(/^.*\/\/[^/]+/, ''); +var pageHistory = utils.get_cookie(LIVECHAT_COOKIE_HISTORY); +var urlHistory = []; +if (pageHistory) { + urlHistory = JSON.parse(pageHistory) || []; +} +if (!_.contains(urlHistory, page)) { + urlHistory.push(page); + while (urlHistory.length > HISTORY_LIMIT) { + urlHistory.shift(); + } + utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(urlHistory), 60 * 60 * 24); // 1 day cookie +} + +var LivechatButton = Widget.extend({ + className: 'openerp o_livechat_button d-print-none', + custom_events: { + 'close_chat_window': '_onCloseChatWindow', + 'post_message_chat_window': '_onPostMessageChatWindow', + 'save_chat_window': '_onSaveChatWindow', + 'updated_unread_counter': '_onUpdatedUnreadCounter', + }, + events: { + 'click': '_openChat' + }, + init: function (parent, serverURL, options) { + this._super(parent); + this.options = _.defaults(options || {}, { + input_placeholder: _t("Ask something ..."), + default_username: _t("Visitor"), + button_text: _t("Chat with one of our collaborators"), + default_message: _t("How may I help you?"), + }); + + this._history = null; + // livechat model + this._livechat = null; + // livechat window + this._chatWindow = null; + this._messages = []; + this._serverURL = serverURL; + }, + willStart: function () { + var self = this; + var cookie = utils.get_cookie('im_livechat_session'); + var ready; + if (!cookie) { + ready = session.rpc('/im_livechat/init', { channel_id: this.options.channel_id }) + .then(function (result) { + if (!result.available_for_me) { + return Promise.reject(); + } + self._rule = result.rule; + }); + } else { + var channel = JSON.parse(cookie); + ready = session.rpc('/mail/chat_history', { uuid: channel.uuid, limit: 100 }) + .then(function (history) { + self._history = history; + }); + } + return ready.then(this._loadQWebTemplate.bind(this)); + }, + start: function () { + this.$el.text(this.options.button_text); + if (this._history) { + _.each(this._history.reverse(), this._addMessage.bind(this)); + this._openChat(); + } else if (!config.device.isMobile && this._rule.action === 'auto_popup') { + var autoPopupCookie = utils.get_cookie('im_livechat_auto_popup'); + if (!autoPopupCookie || JSON.parse(autoPopupCookie)) { + this._autoPopupTimeout = + setTimeout(this._openChat.bind(this), this._rule.auto_popup_timer * 1000); + } + } + this.call('bus_service', 'onNotification', this, this._onNotification); + if (this.options.button_background_color) { + this.$el.css('background-color', this.options.button_background_color); + } + if (this.options.button_text_color) { + this.$el.css('color', this.options.button_text_color); + } + return this._super(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + + /** + * @private + * @param {Object} data + * @param {Object} [options={}] + */ + _addMessage: function (data, options) { + options = _.extend({}, this.options, options, { + serverURL: this._serverURL, + }); + var message = new WebsiteLivechatMessage(this, data, options); + + var hasAlreadyMessage = _.some(this._messages, function (msg) { + return message.getID() === msg.getID(); + }); + if (hasAlreadyMessage) { + return; + } + + if (this._livechat) { + this._livechat.addMessage(message); + } + + if (options && options.prepend) { + this._messages.unshift(message); + } else { + this._messages.push(message); + } + }, + /** + * @private + */ + _askFeedback: function () { + this._chatWindow.$('.o_thread_composer input').prop('disabled', true); + + var feedback = new Feedback(this, this._livechat); + this._chatWindow.replaceContentWith(feedback); + + feedback.on('send_message', this, this._sendMessage); + feedback.on('feedback_sent', this, this._closeChat); + }, + /** + * @private + */ + _closeChat: function () { + this._chatWindow.destroy(); + utils.set_cookie('im_livechat_session', "", -1); // remove cookie + }, + /** + * @private + * @param {Array} notification + */ + _handleNotification: function (notification) { + if (this._livechat && (notification[0] === this._livechat.getUUID())) { + if (notification[1]._type === 'history_command') { // history request + var cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY); + var history = cookie ? JSON.parse(cookie) : []; + session.rpc('/im_livechat/history', { + pid: this._livechat.getOperatorPID()[0], + channel_uuid: this._livechat.getUUID(), + page_history: history, + }); + } else { // normal message + // If message from notif is already in chatter messages, stop handling + if (this._messages.some(message => message.getID() === notification[1].id)) { + return; + } + this._addMessage(notification[1]); + this._renderMessages(); + if (this._chatWindow.isFolded() || !this._chatWindow.isAtBottom()) { + this._livechat.incrementUnreadCounter(); + } + } + } + }, + /** + * @private + */ + _loadQWebTemplate: function () { + return session.rpc('/im_livechat/load_templates').then(function (templates) { + _.each(templates, function (template) { + QWeb.add_template(template); + }); + }); + }, + /** + * @private + */ + _openChat: _.debounce(function () { + if (this._openingChat) { + return; + } + var self = this; + var cookie = utils.get_cookie('im_livechat_session'); + var def; + this._openingChat = true; + clearTimeout(this._autoPopupTimeout); + if (cookie) { + def = Promise.resolve(JSON.parse(cookie)); + } else { + this._messages = []; // re-initialize messages cache + def = session.rpc('/im_livechat/get_session', { + channel_id: this.options.channel_id, + anonymous_name: this.options.default_username, + previous_operator_id: this._get_previous_operator_id(), + }, { shadow: true }); + } + def.then(function (livechatData) { + if (!livechatData || !livechatData.operator_pid) { + self.displayNotification({ + message: _t("No available collaborator, please try again later."), + sticky: true + }); + } else { + self._livechat = new WebsiteLivechat({ + parent: self, + data: livechatData + }); + return self._openChatWindow().then(function () { + if (!self._history) { + self._sendWelcomeMessage(); + } + self._renderMessages(); + self.call('bus_service', 'addChannel', self._livechat.getUUID()); + self.call('bus_service', 'startPolling'); + + utils.set_cookie('im_livechat_session', JSON.stringify(self._livechat.toData()), 60 * 60); + utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60 * 60); + if (livechatData.operator_pid[0]) { + // livechatData.operator_pid contains a tuple (id, name) + // we are only interested in the id + var operatorPidId = livechatData.operator_pid[0]; + var oneWeek = 7 * 24 * 60 * 60; + utils.set_cookie('im_livechat_previous_operator_pid', operatorPidId, oneWeek); + } + }); + } + }).then(function () { + self._openingChat = false; + }).guardedCatch(function () { + self._openingChat = false; + }); + }, 200, true), + /** + * Will try to get a previous operator for this visitor. + * If the visitor already had visitor A, it's better for his user experience + * to get operator A again. + * + * The information is stored in the 'im_livechat_previous_operator_pid' cookie. + * + * @private + * @return {integer} operator_id.partner_id.id if the cookie is set + */ + _get_previous_operator_id: function () { + var cookie = utils.get_cookie('im_livechat_previous_operator_pid'); + if (cookie) { + return cookie; + } + + return null; + }, + /** + * @private + * @return {Promise} + */ + _openChatWindow: function () { + var self = this; + var options = { + displayStars: false, + headerBackgroundColor: this.options.header_background_color, + placeholder: this.options.input_placeholder || "", + titleColor: this.options.title_color, + }; + this._chatWindow = new WebsiteLivechatWindow(this, this._livechat, options); + return this._chatWindow.appendTo($('body')).then(function () { + var cssProps = { bottom: 0 }; + cssProps[_t.database.parameters.direction === 'rtl' ? 'left' : 'right'] = 0; + self._chatWindow.$el.css(cssProps); + self.$el.hide(); + }); + }, + /** + * @private + */ + _renderMessages: function () { + var shouldScroll = !this._chatWindow.isFolded() && this._chatWindow.isAtBottom(); + this._livechat.setMessages(this._messages); + this._chatWindow.render(); + if (shouldScroll) { + this._chatWindow.scrollToBottom(); + } + }, + /** + * @private + * @param {Object} message + * @return {Promise} + */ + _sendMessage: function (message) { + var self = this; + return session + .rpc('/mail/chat_post', { uuid: this._livechat.getUUID(), message_content: message.content }) + .then(function (messageId) { + if (!messageId) { + self.displayNotification({ + message: _t("Session expired... Please refresh and try again."), + sticky: true + }); + self._closeChat(); + } + self._chatWindow.scrollToBottom(); + }); + }, + /** + * @private + */ + _sendWelcomeMessage: function () { + if (this.options.default_message) { + this._addMessage({ + id: '_welcome', + attachment_ids: [], + author_id: this._livechat.getOperatorPID(), + body: this.options.default_message, + channel_ids: [this._livechat.getID()], + date: time.datetime_to_str(new Date()), + }, { prepend: true }); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onCloseChatWindow: function (ev) { + ev.stopPropagation(); + var isComposerDisabled = this._chatWindow.$('.o_thread_composer input').prop('disabled'); + var shouldAskFeedback = !isComposerDisabled && _.find(this._messages, function (message) { + return message.getID() !== '_welcome'; + }); + if (shouldAskFeedback) { + this._chatWindow.toggleFold(false); + this._askFeedback(); + } else { + this._closeChat(); + } + }, + /** + * @private + * @param {Array[]} notifications + */ + _onNotification: function (notifications) { + var self = this; + _.each(notifications, function (notification) { + self._handleNotification(notification); + }); + }, + /** + * @private + * @param {OdooEvent} ev + * @param {Object} ev.data.messageData + */ + _onPostMessageChatWindow: function (ev) { + ev.stopPropagation(); + var self = this; + var messageData = ev.data.messageData; + this._sendMessage(messageData).guardedCatch(function (reason) { + reason.event.preventDefault(); + return self._sendMessage(messageData); // try again just in case + }); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onSaveChatWindow: function (ev) { + ev.stopPropagation(); + utils.set_cookie('im_livechat_session', JSON.stringify(this._livechat.toData()), 60 * 60); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onUpdatedUnreadCounter: function (ev) { + ev.stopPropagation(); + this._chatWindow.renderHeader(); + }, +}); + +/* + * Rating for Livechat + * + * This widget displays the 3 rating smileys, and a textarea to add a reason + * (only for red smiley), and sends the user feedback to the server. + */ +var Feedback = Widget.extend({ + template: 'im_livechat.legacy.im_livechat.FeedBack', + + events: { + 'click .o_livechat_rating_choices img': '_onClickSmiley', + 'click .o_livechat_no_feedback span': '_onClickNoFeedback', + 'click .o_rating_submit_button': '_onClickSend', + 'click .o_email_chat_button': '_onEmailChat', + 'click .o_livechat_email_error .alert-link': '_onTryAgain', + }, + + /** + * @param {?} parent + * @param {im_livechat.legacy.im_livechat.model.WebsiteLivechat} livechat + */ + init: function (parent, livechat) { + this._super(parent); + this._livechat = livechat; + this.server_origin = session.origin; + this.rating = undefined; + this.dp = new concurrency.DropPrevious(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} options + */ + _sendFeedback: function (reason) { + var self = this; + var args = { + uuid: this._livechat.getUUID(), + rate: this.rating, + reason: reason, + }; + this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () { + var emoji = RATING_TO_EMOJI[self.rating] || "??"; + var content = _.str.sprintf(_t("Rating: %s"), emoji); + if (reason) { + content += " \n" + reason; + } + self.trigger('send_message', { content: content, isFeedback: true }); + }); + }, + /** + * @private + */ + _showThanksMessage: function () { + this.$('.o_livechat_rating_box').empty().append($('
', { + text: _t('Thank you for your feedback'), + class: 'text-muted' + })); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickNoFeedback: function () { + this.trigger('feedback_sent'); // will close the chat + }, + /** + * @private + */ + _onClickSend: function () { + this.$('.o_livechat_rating_reason').hide(); + this._showThanksMessage(); + if (_.isNumber(this.rating)) { + this._sendFeedback(this.$('textarea').val()); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickSmiley: function (ev) { + this.rating = parseInt($(ev.currentTarget).data('value')); + this.$('.o_livechat_rating_choices img').removeClass('selected'); + this.$('.o_livechat_rating_choices img[data-value="' + this.rating + '"]').addClass('selected'); + + // only display textearea if bad smiley selected + if (this.rating !== 5) { + this.$('.o_livechat_rating_reason').show(); + } else { + this.$('.o_livechat_rating_reason').hide(); + this._showThanksMessage(); + this._sendFeedback(); + } + }, + /** + * @private + */ + _onEmailChat: function () { + var self = this; + var $email = this.$('#o_email'); + + if (utils.is_email($email.val())) { + $email.removeAttr('title').removeClass('is-invalid').prop('disabled', true); + this.$('.o_email_chat_button').prop('disabled', true); + this._rpc({ + route: '/im_livechat/email_livechat_transcript', + params: { + uuid: this._livechat.getUUID(), + email: $email.val(), + } + }).then(function () { + self.$('.o_livechat_email').html($('', { text: _t('Conversation Sent') })); + }).guardedCatch(function () { + self.$('.o_livechat_email').hide(); + self.$('.o_livechat_email_error').show(); + }); + } else { + $email.addClass('is-invalid').prop('title', _t('Invalid email address')); + } + }, + /** + * @private + */ + _onTryAgain: function () { + this.$('#o_email').prop('disabled', false); + this.$('.o_email_chat_button').prop('disabled', false); + this.$('.o_livechat_email_error').hide(); + this.$('.o_livechat_email').show(); + }, +}); + +return { + LivechatButton: LivechatButton, + Feedback: Feedback, +}; + +}); + +odoo.define('im_livechat.legacy.im_livechat.model.WebsiteLivechat', function (require) { +"use strict"; + +var AbstractThread = require('im_livechat.legacy.mail.model.AbstractThread'); +var ThreadTypingMixin = require('im_livechat.legacy.mail.model.ThreadTypingMixin'); + +var session = require('web.session'); + +/** + * Thread model that represents a livechat on the website-side. This livechat + * is not linked to the mail service. + */ +var WebsiteLivechat = AbstractThread.extend(ThreadTypingMixin, { + + /** + * @override + * @private + * @param {Object} params + * @param {Object} params.data + * @param {boolean} [params.data.folded] states whether the livechat is + * folded or not. It is considered only if this is defined and it is a + * boolean. + * @param {integer} params.data.id the ID of this livechat. + * @param {integer} [params.data.message_unread_counter] the unread counter + * of this livechat. + * @param {Array} params.data.operator_pid + * @param {string} params.data.name the name of this livechat. + * @param {string} [params.data.state] if 'folded', the livechat is folded. + * This is ignored if `folded` is provided and is a boolean value. + * @param {string} params.data.uuid the UUID of this livechat. + * @param {im_livechat.legacy.im_livechat.im_livechat:LivechatButton} params.parent + */ + init: function (params) { + this._super.apply(this, arguments); + ThreadTypingMixin.init.call(this, arguments); + + this._members = []; + this._operatorPID = params.data.operator_pid; + this._uuid = params.data.uuid; + + if (params.data.message_unread_counter !== undefined) { + this._unreadCounter = params.data.message_unread_counter; + } + + if (_.isBoolean(params.data.folded)) { + this._folded = params.data.folded; + } else { + this._folded = params.data.state === 'folded'; + } + + // Necessary for thread typing mixin to display is typing notification + // bar text (at least, for the operator in the members). + this._members.push({ + id: this._operatorPID[0], + name: this._operatorPID[1] + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {im_livechat.legacy.im_livechat.model.WebsiteLivechatMessage[]} + */ + getMessages: function () { + return this._messages; + }, + /** + * @returns {Array} + */ + getOperatorPID: function () { + return this._operatorPID; + }, + /** + * @returns {string} + */ + getUUID: function () { + return this._uuid; + }, + /** + * Increments the unread counter of this livechat by 1 unit. + * + * Note: this public method makes sense because the management of messages + * for website livechat is external. This method should be dropped when + * this class handles messages by itself. + */ + incrementUnreadCounter: function () { + this._incrementUnreadCounter(); + }, + /** + * AKU: hack for the moment + * + * @param {im_livechat.legacy.im_livechat.model.WebsiteLivechatMessage[]} messages + */ + setMessages: function (messages) { + this._messages = messages; + }, + /** + * @returns {Object} + */ + toData: function () { + return { + folded: this.isFolded(), + id: this.getID(), + message_unread_counter: this.getUnreadCounter(), + operator_pid: this.getOperatorPID(), + name: this.getName(), + uuid: this.getUUID(), + }; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override {mail.model.ThreadTypingMixin} + * @private + * @param {Object} params + * @param {boolean} params.isWebsiteUser + * @returns {boolean} + */ + _isTypingMyselfInfo: function (params) { + return params.isWebsiteUser; + }, + /** + * @override {mail.model.ThreadTypingMixin} + * @private + * @param {Object} params + * @param {boolean} params.typing + * @returns {Promise} + */ + _notifyMyselfTyping: function (params) { + return session.rpc('/im_livechat/notify_typing', { + uuid: this.getUUID(), + is_typing: params.typing, + }, { shadow: true }); + }, + /** + * Warn views that the list of users that are currently typing on this + * livechat has been updated. + * + * @override {mail.model.ThreadTypingMixin} + * @private + */ + _warnUpdatedTypingPartners: function () { + this.trigger_up('updated_typing_partners'); + }, + /** + * Warn that the unread counter has been updated on this livechat + * + * @override + * @private + */ + _warnUpdatedUnreadCounter: function () { + this.trigger_up('updated_unread_counter'); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * Override so that it only unregister typing operators. + * + * Note that in the frontend, there is no way to identify a message that is + * from the current user, because there is no partner ID in the session and + * a message with an author sets the partner ID of the author. + * + * @override {mail.model.ThreadTypingMixin} + * @private + * @param {mail.model.AbstractMessage} message + */ + _onTypingMessageAdded: function (message) { + var operatorID = this.getOperatorPID()[0]; + if (message.hasAuthor() && message.getAuthorID() === operatorID) { + this.unregisterTyping({ partnerID: operatorID }); + } + }, +}); + +return WebsiteLivechat; + +}); + +odoo.define('im_livechat.legacy.im_livechat.model.WebsiteLivechatMessage', function (require) { +"use strict"; + +var AbstractMessage = require('im_livechat.legacy.mail.model.AbstractMessage'); + +/** + * This is a message that is handled by im_livechat, without making use of the + * mail.Manager. The purpose of this is to make im_livechat compatible with + * mail.widget.Thread. + * + * @see im_livechat.legacy.mail.model.AbstractMessage for more information. + */ +var WebsiteLivechatMessage = AbstractMessage.extend({ + + /** + * @param {im_livechat.legacy.im_livechat.im_livechat.LivechatButton} parent + * @param {Object} data + * @param {Object} options + * @param {string} options.default_username + * @param {string} options.serverURL + */ + init: function (parent, data, options) { + this._super.apply(this, arguments); + + this._defaultUsername = options.default_username; + this._serverURL = options.serverURL; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Get the relative url of the avatar to display next to the message + * + * @override + * @return {string} + */ + getAvatarSource: function () { + var source = this._serverURL; + if (this.hasAuthor()) { + source += '/web/partner_image/' + this.getAuthorID(); + } else { + source += '/mail/static/src/img/smiley/avatar.jpg'; + } + return source; + }, + /** + * Get the text to display for the author of the message + * + * Rule of precedence for the displayed author:: + * + * author name > default usernane + * + * @override + * @return {string} + */ + getDisplayedAuthor: function () { + return this._super.apply(this, arguments) || this._defaultUsername; + }, + +}); + +return WebsiteLivechatMessage; + +}); + +odoo.define('im_livechat.legacy.im_livechat.WebsiteLivechatWindow', function (require) { +"use strict"; + +var AbstractThreadWindow = require('im_livechat.legacy.mail.AbstractThreadWindow'); + +/** + * This is the widget that represent windows of livechat in the frontend. + * + * @see im_livechat.legacy.mail.AbstractThreadWindow for more information + */ +var LivechatWindow = AbstractThreadWindow.extend({ + events: _.extend(AbstractThreadWindow.prototype.events, { + 'input .o_composer_text_field': '_onInput', + }), + /** + * @override + * @param {im_livechat.legacy.im_livechat.im_livechat:LivechatButton} parent + * @param {im_livechat.legacy.im_livechat.model.WebsiteLivechat} thread + * @param {Object} [options={}] + * @param {string} [options.headerBackgroundColor] + * @param {string} [options.titleColor] + */ + init(parent, thread, options = {}) { + this._super.apply(this, arguments); + this._thread = thread; + }, + /** + * @override + * @return {Promise} + */ + async start() { + await this._super(...arguments); + if (this.options.headerBackgroundColor) { + this.$('.o_thread_window_header').css('background-color', this.options.headerBackgroundColor); + } + if (this.options.titleColor) { + this.$('.o_thread_window_header').css('color', this.options.titleColor); + } + }, + + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + close: function () { + this.trigger_up('close_chat_window'); + }, + /** + * Replace the thread content with provided new content + * + * @param {$.Element} $element + */ + replaceContentWith: function ($element) { + $element.replace(this._threadWidget.$el); + }, + /** + * Warn the parent widget (LivechatButton) + * + * @override + * @param {boolean} folded + */ + toggleFold: function () { + this._super.apply(this, arguments); + this.trigger_up('save_chat_window'); + this.updateVisualFoldState(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + * @param {Object} messageData + */ + _postMessage: function (messageData) { + this.trigger_up('post_message_chat_window', { messageData: messageData }); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the input in the composer changes + * + * @private + */ + _onInput: function () { + if (this.hasThread() && this._thread.hasTypingNotification()) { + var isTyping = this.$input.val().length > 0; + this._thread.setMyselfTyping({ typing: isTyping }); + } + }, +}); + +return LivechatWindow; + +}); + +odoo.define('im_livechat.legacy.mail.model.AbstractThread', function (require) { +"use strict"; + +var Class = require('web.Class'); +var Mixins = require('web.mixins'); + +/** + * Abstract thread is the super class of all threads, either backend threads + * (which are compatible with mail service) or website livechats. + * + * Abstract threads contain abstract messages + */ +var AbstractThread = Class.extend(Mixins.EventDispatcherMixin, { + /** + * @param {Object} params + * @param {Object} params.data + * @param {integer|string} params.data.id the ID of this thread + * @param {string} params.data.name the name of this thread + * @param {string} [params.data.status=''] the status of this thread + * @param {Object} params.parent Object with the event-dispatcher mixin + * (@see {web.mixins.EventDispatcherMixin}) + */ + init: function (params) { + Mixins.EventDispatcherMixin.init.call(this, arguments); + this.setParent(params.parent); + + this._folded = false; // threads are unfolded by default + this._id = params.data.id; + this._name = params.data.name; + this._status = params.data.status || ''; + this._unreadCounter = 0; // amount of messages not yet been read + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Add a message to this thread. + * + * @param {im_livechat.legacy.mail.model.AbstractMessage} message + */ + addMessage: function (message) { + this._addMessage.apply(this, arguments); + this.trigger('message_added', message); + }, + /** + * Updates the folded state of the thread + * + * @param {boolean} folded + */ + fold: function (folded) { + this._folded = folded; + }, + /** + * Get the ID of this thread + * + * @returns {integer|string} + */ + getID: function () { + return this._id; + }, + /** + * @abstract + * @returns {im_livechat.legacy.mail.model.AbstractMessage[]} + */ + getMessages: function () {}, + /** + * Get the name of this thread. If the name of the thread has been created + * by the user from an input, it may be escaped. + * + * @returns {string} + */ + getName: function () { + return this._name; + }, + /** + * Get the status of the thread (e.g. 'online', 'offline', etc.) + * + * @returns {string} + */ + getStatus: function () { + return this._status; + }, + /** + * Returns the title to display in thread window's headers. + * + * @returns {string} the name of the thread by default (see @getName) + */ + getTitle: function () { + return this.getName(); + }, + getType: function () {}, + /** + * @returns {integer} + */ + getUnreadCounter: function () { + return this._unreadCounter; + }, + /** + * @returns {boolean} + */ + hasMessages: function () { + return !_.isEmpty(this.getMessages()); + }, + /** + * States whether this thread is compatible with the 'seen' feature. + * By default, threads do not have thsi feature active. + * @see {im_livechat.legacy.mail.model.ThreadSeenMixin} to enable this feature on a thread. + * + * @returns {boolean} + */ + hasSeenFeature: function () { + return false; + }, + /** + * States whether this thread is folded or not. + * + * @return {boolean} + */ + isFolded: function () { + return this._folded; + }, + /** + * Mark the thread as read, which resets the unread counter to 0. This is + * only performed if the unread counter is not 0. + * + * @returns {Promise} + */ + markAsRead: function () { + if (this._unreadCounter > 0) { + return this._markAsRead(); + } + return Promise.resolve(); + }, + /** + * Post a message on this thread + * + * @returns {Promise} resolved with the message object to be sent to the + * server + */ + postMessage: function () { + return this._postMessage.apply(this, arguments) + .then(this.trigger.bind(this, 'message_posted')); + }, + /** + * Resets the unread counter of this thread to 0. + */ + resetUnreadCounter: function () { + this._unreadCounter = 0; + this._warnUpdatedUnreadCounter(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Add a message to this thread. + * + * @abstract + * @private + * @param {im_livechat.legacy.mail.model.AbstractMessage} message + */ + _addMessage: function (message) {}, + /** + * Increments the unread counter of this thread by 1 unit. + * + * @private + */ + _incrementUnreadCounter: function () { + this._unreadCounter++; + }, + /** + * Mark the thread as read + * + * @private + * @returns {Promise} + */ + _markAsRead: function () { + this.resetUnreadCounter(); + return Promise.resolve(); + }, + /** + * Post a message on this thread + * + * @abstract + * @private + * @returns {Promise} resolved with the message object to be sent to the + * server + */ + _postMessage: function () { + return Promise.resolve(); + }, + /** + * Warn views (e.g. discuss app, thread window, etc.) to update visually + * the unread counter of this thread. + * + * @abstract + * @private + */ + _warnUpdatedUnreadCounter: function () {}, +}); + +return AbstractThread; + +}); + +odoo.define('im_livechat.legacy.mail.model.ThreadTypingMixin', function (require) { +"use strict"; + +var CCThrottleFunction = require('im_livechat.legacy.mail.model.CCThrottleFunction'); +var Timer = require('im_livechat.legacy.mail.model.Timer'); +var Timers = require('im_livechat.legacy.mail.model.Timers'); + +var core = require('web.core'); + +var _t = core._t; + +/** + * Mixin for enabling the "is typing..." notification on a type of thread. + */ +var ThreadTypingMixin = { + // Default partner infos + _DEFAULT_TYPING_PARTNER_ID: '_default', + _DEFAULT_TYPING_PARTNER_NAME: 'Someone', + + /** + * Initialize the internal data for typing feature on threads. + * + * Also listens on some internal events of the thread: + * + * - 'message_added': when a message is added, remove the author in the + * typing partners. + * - 'message_posted': when a message is posted, let the user have the + * possibility to immediately notify if he types something right away, + * instead of waiting for a throttle behaviour. + */ + init: function () { + // Store the last "myself typing" status that has been sent to the + // server. This is useful in order to not notify the same typing + // status multiple times. + this._lastNotifiedMyselfTyping = false; + + // Timer of current user that is typing a very long text. When the + // receivers do not receive any typing notification for a long time, + // they assume that the related partner is no longer typing + // something (e.g. they have closed the browser tab). + // This is a timer to let others know that we are still typing + // something, so that they do not assume we stopped typing + // something. + this._myselfLongTypingTimer = new Timer({ + duration: 50 * 1000, + onTimeout: this._onMyselfLongTypingTimeout.bind(this), + }); + + // Timer of current user that was currently typing something, but + // there is no change on the input for several time. This is used + // in order to automatically notify other users that we have stopped + // typing something, due to making no changes on the composer for + // some time. + this._myselfTypingInactivityTimer = new Timer({ + duration: 5 * 1000, + onTimeout: this._onMyselfTypingInactivityTimeout.bind(this), + }); + + // Timers of users currently typing in the thread. This is useful + // in order to automatically unregister typing users when we do not + // receive any typing notification after a long time. Timers are + // internally indexed by partnerID. The current user is ignored in + // this list of timers. + this._othersTypingTimers = new Timers({ + duration: 60 * 1000, + onTimeout: this._onOthersTypingTimeout.bind(this), + }); + + // Clearable and cancellable throttled version of the + // `doNotifyMyselfTyping` method. (basically `notifyMyselfTyping` + // with slight pre- and post-processing) + // @see {mail.model.ResetableThrottleFunction} + // This is useful when the user posts a message and types something + // else: he must notify immediately that he is typing something, + // instead of waiting for the throttle internal timer. + this._throttleNotifyMyselfTyping = CCThrottleFunction({ + duration: 2.5 * 1000, + func: this._onNotifyMyselfTyping.bind(this), + }); + + // This is used to track the order of registered partners typing + // something, in order to display the oldest typing partners. + this._typingPartnerIDs = []; + + this.on('message_added', this, this._onTypingMessageAdded); + this.on('message_posted', this, this._onTypingMessagePosted); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Get the text to display when some partners are typing something on the + * thread: + * + * - single typing partner: + * + * A is typing... + * + * - two typing partners: + * + * A and B are typing... + * + * - three or more typing partners: + * + * A, B and more are typing... + * + * The choice of the members name for display is not random: it displays + * the user that have been typing for the longest time. Also, this function + * is hard-coded to display at most 2 partners. This limitation comes from + * how translation works in Odoo, for which unevaluated string cannot be + * translated. + * + * @returns {string} list of members that are typing something on the thread + * (excluding the current user). + */ + getTypingMembersToText: function () { + var typingPartnerIDs = this._typingPartnerIDs; + var typingMembers = _.filter(this._members, function (member) { + return _.contains(typingPartnerIDs, member.id); + }); + var sortedTypingMembers = _.sortBy(typingMembers, function (member) { + return _.indexOf(typingPartnerIDs, member.id); + }); + var displayableTypingMembers = sortedTypingMembers.slice(0, 3); + + if (displayableTypingMembers.length === 0) { + return ''; + } else if (displayableTypingMembers.length === 1) { + return _.str.sprintf(_t("%s is typing..."), displayableTypingMembers[0].name); + } else if (displayableTypingMembers.length === 2) { + return _.str.sprintf(_t("%s and %s are typing..."), + displayableTypingMembers[0].name, + displayableTypingMembers[1].name); + } else { + return _.str.sprintf(_t("%s, %s and more are typing..."), + displayableTypingMembers[0].name, + displayableTypingMembers[1].name); + } + }, + /** + * Threads with this mixin have the typing notification feature + * + * @returns {boolean} + */ + hasTypingNotification: function () { + return true; + }, + /** + * Tells if someone other than current user is typing something on this + * thread. + * + * @returns {boolean} + */ + isSomeoneTyping: function () { + return !(_.isEmpty(this._typingPartnerIDs)); + }, + /** + * Register someone that is currently typing something in this thread. + * If this is the current user that is typing something, don't do anything + * (we do not have to display anything) + * + * This method is ignored if we try to register the current user. + * + * @param {Object} params + * @param {integer} params.partnerID ID of the partner linked to the user + * currently typing something on the thread. + */ + registerTyping: function (params) { + if (this._isTypingMyselfInfo(params)) { + return; + } + var partnerID = params.partnerID; + this._othersTypingTimers.registerTimer({ + timeoutCallbackArguments: [partnerID], + timerID: partnerID, + }); + if (_.contains(this._typingPartnerIDs, partnerID)) { + return; + } + this._typingPartnerIDs.push(partnerID); + this._warnUpdatedTypingPartners(); + }, + /** + * This method must be called when the user starts or stops typing something + * in the composer of the thread. + * + * @param {Object} params + * @param {boolean} params.typing tell whether the current is typing or not. + */ + setMyselfTyping: function (params) { + var typing = params.typing; + if (this._lastNotifiedMyselfTyping === typing) { + this._throttleNotifyMyselfTyping.cancel(); + } else { + this._throttleNotifyMyselfTyping(params); + } + + if (typing) { + this._myselfTypingInactivityTimer.reset(); + } else { + this._myselfTypingInactivityTimer.clear(); + } + }, + /** + * Unregister someone from currently typing something in this thread. + * + * @param {Object} params + * @param {integer} params.partnerID ID of the partner related to the user + * that is currently typing something + */ + unregisterTyping: function (params) { + var partnerID = params.partnerID; + this._othersTypingTimers.unregisterTimer({ timerID: partnerID }); + if (!_.contains(this._typingPartnerIDs, partnerID)) { + return; + } + this._typingPartnerIDs = _.reject(this._typingPartnerIDs, function (id) { + return id === partnerID; + }); + this._warnUpdatedTypingPartners(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Tells whether the provided information on a partner is related to the + * current user or not. + * + * @abstract + * @private + * @param {Object} params + * @param {integer} params.partner ID of partner to check + */ + _isTypingMyselfInfo: function (params) { + return false; + }, + /** + * Notify to the server that the current user either starts or stops typing + * something. + * + * @abstract + * @private + * @param {Object} params + * @param {boolean} params.typing whether we are typing something or not + * @returns {Promise} resolved if the server is notified, rejected + * otherwise + */ + _notifyMyselfTyping: function (params) { + return Promise.resolve(); + }, + /** + * Warn views that the list of users that are currently typing on this + * thread has been updated. + * + * @abstract + * @private + */ + _warnUpdatedTypingPartners: function () {}, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when current user is typing something for a long time. In order + * to not let other users assume that we are no longer typing something, we + * must notify again that we are typing something. + * + * @private + */ + _onMyselfLongTypingTimeout: function () { + this._throttleNotifyMyselfTyping.clear(); + this._throttleNotifyMyselfTyping({ typing: true }); + }, + /** + * Called when current user has something typed in the composer, but is + * inactive for some time. In this case, he automatically notifies that he + * is no longer typing something + * + * @private + */ + _onMyselfTypingInactivityTimeout: function () { + this._throttleNotifyMyselfTyping.clear(); + this._throttleNotifyMyselfTyping({ typing: false }); + }, + /** + * Called by throttled version of notify myself typing + * + * Notify to the server that the current user either starts or stops typing + * something. Remember last notified stuff from the server, and update + * related typing timers. + * + * @private + * @param {Object} params + * @param {boolean} params.typing whether we are typing something or not. + */ + _onNotifyMyselfTyping: function (params) { + var typing = params.typing; + this._lastNotifiedMyselfTyping = typing; + this._notifyMyselfTyping(params); + if (typing) { + this._myselfLongTypingTimer.reset(); + } else { + this._myselfLongTypingTimer.clear(); + } + }, + /** + * Called when current user do not receive a typing notification of someone + * else typing for a long time. In this case, we assume that this person is + * no longer typing something. + * + * @private + * @param {integer} partnerID partnerID of the person we assume he is no + * longer typing something. + */ + _onOthersTypingTimeout: function (partnerID) { + this.unregisterTyping({ partnerID: partnerID }); + }, + /** + * Called when a new message is added to the thread + * On receiving a message from a typing partner, unregister this partner + * from typing partners (otherwise, it will still display it until timeout). + * + * @private + * @param {mail.model.AbstractMessage} message + */ + _onTypingMessageAdded: function (message) { + var partnerID = message.hasAuthor() ? + message.getAuthorID() : + this._DEFAULT_TYPING_PARTNER_ID; + this.unregisterTyping({ partnerID: partnerID }); + }, + /** + * Called when current user has posted a message on this thread. + * + * The current user receives the possibility to immediately notify the + * other users if he is typing something else. + * + * Refresh the context for the current user to notify that he starts or + * stops typing something. In other words, when this function is called and + * then the current user types something, it immediately notifies the + * server as if it is the first time he is typing something. + * + * @private + */ + _onTypingMessagePosted: function () { + this._lastNotifiedMyselfTyping = false; + this._throttleNotifyMyselfTyping.clear(); + this._myselfLongTypingTimer.clear(); + this._myselfTypingInactivityTimer.clear(); + }, +}; + +return ThreadTypingMixin; + +}); + +odoo.define('im_livechat.legacy.mail.model.AbstractMessage', function (require) { +"use strict"; + +var mailUtils = require('mail.utils'); + +var Class = require('web.Class'); +var core = require('web.core'); +var session = require('web.session'); +var time = require('web.time'); + +var _t = core._t; + +/** + * This is an abstract class for modeling messages in JS. + * The purpose of this interface is to make im_livechat compatible with + * mail.widget.Thread, as this widget was designed to work with messages that + * are instances of mail.model.Messages. + * + * Ideally, im_livechat should also handle mail.model.Message, but this is not + * feasible for the moment, as mail.model.Message requires mail.Manager to work, + * and this module should not leak outside of the backend, hence the use of + * mail.model.AbstractMessage as a work-around. + */ +var AbstractMessage = Class.extend({ + + /** + * @param {Widget} parent + * @param {Object} data + * @param {Array} [data.attachment_ids=[]] + * @param {Array} [data.author_id] + * @param {string} [data.body = ""] + * @param {string} [data.date] the server-format date time of the message. + * If not provided, use current date time for this message. + * @param {integer} data.id + * @param {boolean} [data.is_discussion = false] + * @param {boolean} [data.is_notification = false] + * @param {string} [data.message_type = undefined] + */ + init: function (parent, data) { + this._attachmentIDs = data.attachment_ids || []; + this._body = data.body || ""; + // by default: current datetime + this._date = data.date ? moment(time.str_to_datetime(data.date)) : moment(); + this._id = data.id; + this._isDiscussion = data.is_discussion; + this._isNotification = data.is_notification; + this._serverAuthorID = data.author_id; + this._type = data.message_type || undefined; + + this._processAttachmentURL(); + this._attachmentIDs.forEach(function (attachment) { + attachment.filename = attachment.filename || attachment.name || _t("unnamed"); + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Get the list of files attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getAttachments: function () { + return this._attachmentIDs; + }, + /** + * Get the server ID (number) of the author of this message + * If there are no author, return -1; + * + * @return {integer} + */ + getAuthorID: function () { + if (!this.hasAuthor()) { + return -1; + } + return this._serverAuthorID[0]; + }, + /** + * Threads do not have an im status by default + * + * @return {undefined} + */ + getAuthorImStatus: function () { + return undefined; + }, + /** + * Get the relative url of the avatar to display next to the message + * + * @abstract + * @return {string} + */ + getAvatarSource: function () { + if (this.hasAuthor()) { + return '/web/image/res.partner/' + this.getAuthorID() + '/image_128'; + } + }, + /** + * Get the body content of this message + * + * @return {string} + */ + getBody: function () { + return this._body; + }, + /** + * @return {moment} + */ + getDate: function () { + return this._date; + }, + /** + * Get the date day of this message + * + * @return {string} + */ + getDateDay: function () { + var date = this.getDate().format('YYYY-MM-DD'); + if (date === moment().format('YYYY-MM-DD')) { + return _t("Today"); + } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { + return _t("Yesterday"); + } + return this.getDate().format('LL'); + }, + /** + * Get the name of the author, if there is an author of this message + * If there are no author of this message, returns 'null' + * + * @return {string} + */ + getDisplayedAuthor: function () { + return this.hasAuthor() ? this._getAuthorName() : null; + }, + /** + * Get the server ID (number) of this message + * + * @override + * @return {integer} + */ + getID: function () { + return this._id; + }, + /** + * Get the list of images attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getImageAttachments: function () { + return _.filter(this.getAttachments(), function (attachment) { + return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; + }); + }, + /** + * Get the list of non-images attached to this message. + * Note that attachments are stored with server-format + * + * @return {Object[]} + */ + getNonImageAttachments: function () { + return _.difference(this.getAttachments(), this.getImageAttachments()); + }, + /** + * Gets the class to use as the notification icon. + * + * @returns {string} + */ + getNotificationIcon() { + if (!this.hasNotificationsError()) { + return 'fa fa-envelope-o'; + } + return 'fa fa-envelope'; + }, + /** + * Gets the list of notifications of this message, in no specific order. + * By default messages do not have notifications. + * + * @returns {Object[]} + */ + getNotifications() { + return []; + }, + /** + * Gets the text to display next to the notification icon. + * + * @returns {string} + */ + getNotificationText() { + return ''; + }, + /** + * Get the time elapsed between sent message and now + * + * @return {string} + */ + getTimeElapsed: function () { + return mailUtils.timeFromNow(this.getDate()); + }, + /** + * Get the type of message (e.g. 'comment', 'email', 'notification', ...) + * By default, messages are of type 'undefined' + * + * @override + * @return {string|undefined} + */ + getType: function () { + return this._type; + }, + /** + * State whether this message contains some attachments. + * + * @override + * @return {boolean} + */ + hasAttachments: function () { + return this.getAttachments().length > 0; + }, + /** + * State whether this message has an author + * + * @return {boolean} + */ + hasAuthor: function () { + return !!(this._serverAuthorID && this._serverAuthorID[0]); + }, + /** + * State whether this message has an email of its sender. + * By default, messages do not have any email of its sender. + * + * @return {string} + */ + hasEmailFrom: function () { + return false; + }, + /** + * State whether this image contains images attachments + * + * @return {boolean} + */ + hasImageAttachments: function () { + return _.some(this.getAttachments(), function (attachment) { + return attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'; + }); + }, + /** + * State whether this image contains non-images attachments + * + * @return {boolean} + */ + hasNonImageAttachments: function () { + return _.some(this.getAttachments(), function (attachment) { + return !(attachment.mimetype && attachment.mimetype.split('/')[0] === 'image'); + }); + }, + /** + * States whether this message has some notifications. + * + * @returns {boolean} + */ + hasNotifications() { + return this.getNotifications().length > 0; + }, + /** + * States whether this message has notifications that are in error. + * + * @returns {boolean} + */ + hasNotificationsError() { + return this.getNotifications().some(notif => + notif.notification_status === 'exception' || + notif.notification_status === 'bounce' + ); + }, + /** + * State whether this message originates from a channel. + * By default, messages do not originate from a channel. + * + * @override + * @return {boolean} + */ + originatesFromChannel: function () { + return false; + }, + /** + * State whether this message has a subject + * By default, messages do not have any subject. + * + * @return {boolean} + */ + hasSubject: function () { + return false; + }, + /** + * State whether this message is empty + * + * @return {boolean} + */ + isEmpty: function () { + return !this.hasTrackingValues() && + !this.hasAttachments() && + !this.getBody(); + }, + /** + * By default, messages do not have any subtype description + * + * @return {boolean} + */ + hasSubtypeDescription: function () { + return false; + }, + /** + * State whether this message contains some tracking values + * By default, messages do not have any tracking values. + * + * @return {boolean} + */ + hasTrackingValues: function () { + return false; + }, + /** + * State whether this message is a discussion + * + * @return {boolean} + */ + isDiscussion: function () { + return this._isDiscussion; + }, + /** + * State whether this message is linked to a document thread + * By default, messages are not linked to a document thread. + * + * @return {boolean} + */ + isLinkedToDocumentThread: function () { + return false; + }, + /** + * State whether this message is needaction + * By default, messages are not needaction. + * + * @return {boolean} + */ + isNeedaction: function () { + return false; + }, + /** + * State whether this message is a note (i.e. a message from "Log note") + * + * @return {boolean} + */ + isNote: function () { + return this._isNote; + }, + /** + * State whether this message is a notification + * + * User notifications are defined as either + * - notes + * - pushed to user Inbox or email through classic notification process + * - not linked to any document, meaning model and res_id are void + * + * This is useful in order to display white background for user + * notifications in chatter + * + * @returns {boolean} + */ + isNotification: function () { + return this._isNotification; + }, + /** + * State whether this message is starred + * By default, messages are not starred. + * + * @return {boolean} + */ + isStarred: function () { + return false; + }, + /** + * State whether this message is a system notification + * By default, messages are not system notifications + * + * @override + * @return {boolean} + */ + isSystemNotification: function () { + return false; + }, + /** + * States whether the current message needs moderation in general. + * By default, messages do not require any moderation. + * + * @returns {boolean} + */ + needsModeration: function () { + return false; + }, + /** + * @params {integer[]} attachmentIDs + */ + removeAttachments: function (attachmentIDs) { + this._attachmentIDs = _.reject(this._attachmentIDs, function (attachment) { + return _.contains(attachmentIDs, attachment.id); + }); + }, + /** + * State whether this message should redirect to the author + * when clicking on the author of this message. + * + * Do not redirect on author clicked of self-posted messages. + * + * @return {boolean} + */ + shouldRedirectToAuthor: function () { + return !this._isMyselfAuthor(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Get the name of the author of this message. + * If there are no author of this messages, returns '' (empty string). + * + * @private + * @returns {string} + */ + _getAuthorName: function () { + if (!this.hasAuthor()) { + return ""; + } + return this._serverAuthorID[1]; + }, + /** + * State whether the current user is the author of this message + * + * @private + * @return {boolean} + */ + _isMyselfAuthor: function () { + return this.hasAuthor() && (this.getAuthorID() === session.partner_id); + }, + /** + * Compute url of attachments of this message + * + * @private + */ + _processAttachmentURL: function () { + _.each(this.getAttachments(), function (attachment) { + attachment.url = '/web/content/' + attachment.id + '?download=true'; + }); + }, + +}); + +return AbstractMessage; + +}); + +odoo.define('im_livechat.legacy.mail.AbstractThreadWindow', function (require) { +"use strict"; + +var ThreadWidget = require('im_livechat.legacy.mail.widget.Thread'); + +var config = require('web.config'); +var core = require('web.core'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * This is an abstract widget for rendering thread windows. + * AbstractThreadWindow is kept for legacy reasons. + */ +var AbstractThreadWindow = Widget.extend({ + template: 'im_livechat.legacy.mail.AbstractThreadWindow', + custom_events: { + document_viewer_closed: '_onDocumentViewerClose', + }, + events: { + 'click .o_thread_window_close': '_onClickClose', + 'click .o_thread_window_title': '_onClickFold', + 'click .o_composer_text_field': '_onComposerClick', + 'click .o_mail_thread': '_onThreadWindowClicked', + 'keydown .o_composer_text_field': '_onKeydown', + 'keypress .o_composer_text_field': '_onKeypress', + }, + FOLD_ANIMATION_DURATION: 200, // duration in ms for (un)fold transition + HEIGHT_OPEN: '400px', // height in px of thread window when open + HEIGHT_FOLDED: '34px', // height, in px, of thread window when folded + /** + * Children of this class must make use of `thread`, which is an object that + * represent the thread that is linked to this thread window. + * + * If no thread is provided, this will represent the "blank" thread window. + * + * @abstract + * @param {Widget} parent + * @param {im_livechat.legacy.mail.model.AbstractThread} [thread=null] the thread that this + * thread window is linked to. If not set, it is the "blank" thread + * window. + * @param {Object} [options={}] + * @param {im_livechat.legacy.mail.model.AbstractThread} [options.thread] + */ + init: function (parent, thread, options) { + this._super(parent); + + this.options = _.defaults(options || {}, { + autofocus: true, + displayStars: true, + displayReplyIcons: false, + displayNotificationIcons: false, + placeholder: _t("Say something"), + }); + + this._hidden = false; + this._thread = thread || null; + + this._debouncedOnScroll = _.debounce(this._onScroll.bind(this), 100); + + if (!this.hasThread()) { + // internal fold state of thread window without any thread + this._folded = false; + } + }, + start: function () { + var self = this; + this.$input = this.$('.o_composer_text_field'); + this.$header = this.$('.o_thread_window_header'); + var options = { + displayMarkAsRead: false, + displayStars: this.options.displayStars, + }; + if (this._thread && this._thread._type === 'document_thread') { + options.displayDocumentLinks = false; + } + this._threadWidget = new ThreadWidget(this, options); + + // animate the (un)folding of thread windows + this.$el.css({ transition: 'height ' + this.FOLD_ANIMATION_DURATION + 'ms linear' }); + if (this.isFolded()) { + this.$el.css('height', this.HEIGHT_FOLDED); + } else if (this.options.autofocus) { + this._focusInput(); + } + if (!config.device.isMobile) { + var margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; + this.$el.css(margin_dir, $.position.scrollbarWidth()); + } + var def = this._threadWidget.replace(this.$('.o_thread_window_content')).then(function () { + self._threadWidget.$el.on('scroll', self, self._debouncedOnScroll); + }); + return Promise.all([this._super(), def]); + }, + /** + * @override + */ + do_hide: function () { + this._hidden = true; + this._super.apply(this, arguments); + }, + /** + * @override + */ + do_show: function () { + this._hidden = false; + this._super.apply(this, arguments); + }, + /** + * @override + */ + do_toggle: function (display) { + this._hidden = _.isBoolean(display) ? !display : !this._hidden; + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Close this window + * + * @abstract + */ + close: function () {}, + /** + * Get the ID of the thread window, which is equivalent to the ID of the + * thread related to this window + * + * @returns {integer|string} + */ + getID: function () { + return this._getThreadID(); + }, + /** + * @returns {mail.model.Thread|undefined} + */ + getThread: function () { + if (!this.hasThread()) { + return undefined; + } + return this._thread; + }, + /** + * Get the status of the thread, such as the im status of a DM chat + * ('online', 'offline', etc.). If this window has no thread, returns + * `undefined`. + * + * @returns {string|undefined} + */ + getThreadStatus: function () { + if (!this.hasThread()) { + return undefined; + } + return this._thread.getStatus(); + }, + /** + * Get the title of the thread window, which usually contains the name of + * the thread. + * + * @returns {string} + */ + getTitle: function () { + if (!this.hasThread()) { + return _t("Undefined"); + } + return this._thread.getTitle(); + }, + /** + * Get the unread counter of the related thread. If there are no thread + * linked to this window, returns 0. + * + * @returns {integer} + */ + getUnreadCounter: function () { + if (!this.hasThread()) { + return 0; + } + return this._thread.getUnreadCounter(); + }, + /** + * States whether this thread window is related to a thread or not. + * + * This is useful in order to provide specific behaviour for thread windows + * without any thread, e.g. let them open a thread from this "blank" thread + * window. + * + * @returns {boolean} + */ + hasThread: function () { + return !! this._thread; + }, + /** + * Tells whether the bottom of the thread in the thread window is visible + * or not. + * + * @returns {boolean} + */ + isAtBottom: function () { + return this._threadWidget.isAtBottom(); + }, + /** + * State whether the related thread is folded or not. If there are no + * thread related to this window, it means this is the "blank" thread + * window, therefore we use the internal folded state. + * + * @returns {boolean} + */ + isFolded: function () { + if (!this.hasThread()) { + return this._folded; + } + return this._thread.isFolded(); + }, + /** + * States whether the current environment is in mobile or not. This is + * useful in order to customize the template rendering for mobile view. + * + * @returns {boolean} + */ + isMobile: function () { + return config.device.isMobile; + }, + /** + * States whether the thread window is hidden or not. + * + * @returns {boolean} + */ + isHidden: function () { + return this._hidden; + }, + /** + * States whether the input of the thread window should be displayed or not. + * By default, any thread window with a thread needs a composer. + * + * @returns {boolean} + */ + needsComposer: function () { + return this.hasThread(); + }, + /** + * Render the thread window + */ + render: function () { + this.renderHeader(); + if (this.hasThread()) { + this._threadWidget.render(this._thread, { displayLoadMore: false }); + } + }, + /** + * Render the header of this thread window. + * This is useful when some information on the header have be updated such + * as the status or the title of the thread that have changed. + * + * @private + */ + renderHeader: function () { + var options = this._getHeaderRenderingOptions(); + this.$header.html( + QWeb.render('im_livechat.legacy.mail.AbstractThreadWindow.HeaderContent', options)); + }, + /** + * Scroll to the bottom of the thread in the thread window + */ + scrollToBottom: function () { + this._threadWidget.scrollToBottom(); + }, + /** + * Toggle the fold state of this thread window. Also update the fold state + * of the thread model. If the boolean parameter `folded` is provided, it + * folds/unfolds the window when it is set/unset. + * + * @param {boolean} [folded] if not a boolean, toggle the fold state. + * Otherwise, fold/unfold the window if set/unset. + */ + toggleFold: function (folded) { + if (!_.isBoolean(folded)) { + folded = !this.isFolded(); + } + this._updateThreadFoldState(folded); + }, + /** + * Update the visual state of the window so that it matched the internal + * fold state. This is useful in case the related thread has its fold state + * that has been changed. + */ + updateVisualFoldState: function () { + if (!this.isFolded()) { + this._threadWidget.scrollToBottom(); + if (this.options.autofocus) { + this._focusInput(); + } + } + var height = this.isFolded() ? this.HEIGHT_FOLDED : this.HEIGHT_OPEN; + this.$el.css({ height: height }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Set the focus on the composer of the thread window. This operation is + * ignored in mobile context. + * + * @private + * Set the focus on the input of the window + */ + _focusInput: function () { + if ( + config.device.touch && + config.device.size_class <= config.device.SIZES.SM + ) { + return; + } + this.$input.focus(); + }, + /** + * Returns the options used by the rendering of the window's header + * + * @private + * @returns {Object} + */ + _getHeaderRenderingOptions: function () { + return { + status: this.getThreadStatus(), + thread: this.getThread(), + title: this.getTitle(), + unreadCounter: this.getUnreadCounter(), + widget: this, + }; + }, + /** + * Get the ID of the related thread. + * If this window is not related to a thread, it means this is the "blank" + * thread window, therefore it returns "_blank" as its ID. + * + * @private + * @returns {integer|string} the threadID, or '_blank' for the window that + * is not related to any thread. + */ + _getThreadID: function () { + if (!this.hasThread()) { + return '_blank'; + } + return this._thread.getID(); + }, + /** + * Tells whether there is focus on this thread. Note that a thread that has + * the focus means the input has focus. + * + * @private + * @returns {boolean} + */ + _hasFocus: function () { + return this.$input.is(':focus'); + }, + /** + * Post a message on this thread window, and auto-scroll to the bottom of + * the thread. + * + * @private + * @param {Object} messageData + */ + _postMessage: function (messageData) { + var self = this; + if (!this.hasThread()) { + return; + } + this._thread.postMessage(messageData) + .then(function () { + self._threadWidget.scrollToBottom(); + }); + }, + /** + * Update the fold state of the thread. + * + * This function is called when toggling the fold state of this window. + * If there is no thread linked to this window, it means this is the + * "blank" thread window, therefore we use the internal state 'folded' + * + * @private + * @param {boolean} folded + */ + _updateThreadFoldState: function (folded) { + if (this.hasThread()) { + this._thread.fold(folded); + } else { + this._folded = folded; + this.updateVisualFoldState(); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Close the thread window. + * Mark the thread as read if the thread window was open. + * + * @private + * @param {MouseEvent} ev + */ + _onClickClose: function (ev) { + ev.stopPropagation(); + ev.preventDefault(); + if ( + this.hasThread() && + this._thread.getUnreadCounter() > 0 && + !this.isFolded() + ) { + this._thread.markAsRead(); + } + this.close(); + }, + /** + * Fold/unfold the thread window. + * Also mark the thread as read. + * + * @private + */ + _onClickFold: function () { + if (!config.device.isMobile) { + this.toggleFold(); + } + }, + /** + * Called when the composer is clicked -> forces focus on input even if + * jquery's blockUI is enabled. + * + * @private + * @param {Event} ev + */ + _onComposerClick: function (ev) { + if ($(ev.target).closest('a, button').length) { + return; + } + this._focusInput(); + }, + /** + * @private + */ + _onDocumentViewerClose: function () { + this._focusInput(); + }, + /** + * Called when typing something on the composer of this thread window. + * + * @private + * @param {KeyboardEvent} ev + */ + _onKeydown: function (ev) { + ev.stopPropagation(); // to prevent jquery's blockUI to cancel event + // ENTER key (avoid requiring jquery ui for external livechat) + if (ev.which === 13) { + var content = _.str.trim(this.$input.val()); + var messageData = { + content: content, + attachment_ids: [], + partner_ids: [], + }; + this.$input.val(''); + if (content) { + this._postMessage(messageData); + } + } + }, + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeypress: function (ev) { + ev.stopPropagation(); // to prevent jquery's blockUI to cancel event + }, + /** + * @private + */ + _onScroll: function () { + if (this.hasThread() && this.isAtBottom()) { + this._thread.markAsRead(); + } + }, + /** + * When a thread window is clicked on, we want to give the focus to the main + * input. An exception is made when the user is selecting something. + * + * @private + */ + _onThreadWindowClicked: function () { + var selectObj = window.getSelection(); + if (selectObj.anchorOffset === selectObj.focusOffset) { + this.$input.focus(); + } + }, +}); + +return AbstractThreadWindow; + +}); + +odoo.define('im_livechat.legacy.mail.model.CCThrottleFunctionObject', function (require) { +"use strict"; + +var Class = require('web.Class'); + +/** + * This object models the behaviour of the clearable and cancellable (CC) + * throttle version of a provided function. + */ +var CCThrottleFunctionObject = Class.extend({ + + /** + * @param {Object} params + * @param {integer} params.duration duration of the 'cooldown' phase, i.e. + * the minimum duration between the most recent function call that has + * been made and the following function call. + * @param {function} params.func provided function for making the CC + * throttled version. + */ + init: function (params) { + this._arguments = undefined; + this._cooldownTimeout = undefined; + this._duration = params.duration; + this._func = params.func; + this._shouldCallFunctionAfterCD = false; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Cancel any buffered function call, but keep the cooldown phase running. + */ + cancel: function () { + this._arguments = undefined; + this._shouldCallFunctionAfterCD = false; + }, + /** + * Clear the internal throttle timer, so that the following function call + * is immediate. For instance, if there is a cooldown stage, it is aborted. + */ + clear: function () { + if (this._cooldownTimeout) { + clearTimeout(this._cooldownTimeout); + this._onCooldownTimeout(); + } + }, + /** + * Called when there is a call to the function. This function is throttled, + * so the time it is called depends on whether the "cooldown stage" occurs + * or not: + * + * - no cooldown stage: function is called immediately, and it starts + * the cooldown stage when successful. + * - in cooldown stage: function is called when the cooldown stage has + * ended from timeout. + * + * Note that after the cooldown stage, only the last attempted function + * call will be considered. + */ + do: function () { + this._arguments = Array.prototype.slice.call(arguments); + if (this._cooldownTimeout === undefined) { + this._callFunction(); + } else { + this._shouldCallFunctionAfterCD = true; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Immediately calls the function with arguments of last buffered function + * call. It initiates the cooldown stage after this function call. + * + * @private + */ + _callFunction: function () { + this._func.apply(null, this._arguments); + this._cooldown(); + }, + /** + * Called when the function has been successfully called. The following + * calls to the function with this object should suffer a "cooldown stage", + * which prevents the function from being called until this stage has ended. + * + * @private + */ + _cooldown: function () { + this.cancel(); + this._cooldownTimeout = setTimeout( + this._onCooldownTimeout.bind(this), + this._duration + ); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the cooldown stage ended from timeout. Calls the function if + * a function call was buffered. + * + * @private + */ + _onCooldownTimeout: function () { + if (this._shouldCallFunctionAfterCD) { + this._callFunction(); + } else { + this._cooldownTimeout = undefined; + } + }, +}); + +return CCThrottleFunctionObject; + +}); + +odoo.define('im_livechat.legacy.mail.model.CCThrottleFunction', function (require) { +"use strict"; + +var CCThrottleFunctionObject = require('im_livechat.legacy.mail.model.CCThrottleFunctionObject'); + +/** + * A function that creates a cancellable and clearable (CC) throttle version + * of a provided function. + * + * This throttle mechanism allows calling a function at most once during a + * certain period: + * + * - When a function call is made, it enters a 'cooldown' phase, in which any + * attempt to call the function is buffered until the cooldown phase ends. + * - At most 1 function call can be buffered during the cooldown phase, and the + * latest one in this phase will be considered at its end. + * - When a cooldown phase ends, any buffered function call will be performed + * and another cooldown phase will follow up. + * + * This throttle version has the following interesting properties: + * + * - cancellable: it allows removing a buffered function call during the + * cooldown phase, but it keeps the cooldown phase running. + * - clearable: it allows to clear the internal clock of the throttled function, + * so that any cooldown phase is immediately ending. + * + * @param {Object} params + * @param {integer} params.duration a duration for the throttled behaviour, + * in milli-seconds. + * @param {function} params.func the function to throttle + * @returns {function} the cancellable and clearable throttle version of the + * provided function in argument. + */ +var CCThrottleFunction = function (params) { + var duration = params.duration; + var func = params.func; + + var throttleFunctionObject = new CCThrottleFunctionObject({ + duration: duration, + func: func, + }); + + var callable = function () { + return throttleFunctionObject.do.apply(throttleFunctionObject, arguments); + }; + callable.cancel = function () { + throttleFunctionObject.cancel(); + }; + callable.clear = function () { + throttleFunctionObject.clear(); + }; + + return callable; +}; + +return CCThrottleFunction; + +}); + +odoo.define('im_livechat.legacy.mail.model.Timer', function (require) { +"use strict"; + +var Class = require('web.Class'); + +/** + * This class creates a timer which, when times out, calls a function. + */ +var Timer = Class.extend({ + + /** + * Instantiate a new timer. Note that the timer is not started on + * initialization (@see start method). + * + * @param {Object} params + * @param {number} params.duration duration of timer before timeout in + * milli-seconds. + * @param {function} params.onTimeout function that is called when the + * timer times out. + */ + init: function (params) { + this._duration = params.duration; + this._timeout = undefined; + this._timeoutCallback = params.onTimeout; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Clears the countdown of the timer. + */ + clear: function () { + clearTimeout(this._timeout); + }, + /** + * Resets the timer, i.e. resets its duration. + */ + reset: function () { + this.clear(); + this.start(); + }, + /** + * Starts the timer, i.e. after a certain duration, it times out and calls + * a function back. + */ + start: function () { + this._timeout = setTimeout(this._onTimeout.bind(this), this._duration); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * Called when the timer times out, calls back a function on timeout. + * + * @private + */ + _onTimeout: function () { + this._timeoutCallback(); + }, + +}); + +return Timer; + +}); + +odoo.define('im_livechat.legacy.mail.model.Timers', function (require) { +"use strict"; + +var Timer = require('im_livechat.legacy.mail.model.Timer'); + +var Class = require('web.Class'); + +/** + * This class lists several timers that use a same callback and duration. + */ +var Timers = Class.extend({ + + /** + * Instantiate a new list of timers + * + * @param {Object} params + * @param {integer} params.duration duration of the underlying timers from + * start to timeout, in milli-seconds. + * @param {function} params.onTimeout a function to call back for underlying + * timers on timeout. + */ + init: function (params) { + this._duration = params.duration; + this._timeoutCallback = params.onTimeout; + this._timers = {}; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Register a timer with ID `timerID` to start. + * + * - an already registered timer with this ID is reset. + * - (optional) can provide a list of arguments that is passed to the + * function callback when timer times out. + * + * @param {Object} params + * @param {Array} [params.timeoutCallbackArguments] + * @param {integer} params.timerID + */ + registerTimer: function (params) { + var timerID = params.timerID; + if (this._timers[timerID]) { + this._timers[timerID].clear(); + } + var timerParams = { + duration: this._duration, + onTimeout: this._timeoutCallback, + }; + if ('timeoutCallbackArguments' in params) { + timerParams.onTimeout = this._timeoutCallback.bind.apply( + this._timeoutCallback, + [null].concat(params.timeoutCallbackArguments) + ); + } else { + timerParams.onTimeout = this._timeoutCallback; + } + this._timers[timerID] = new Timer(timerParams); + this._timers[timerID].start(); + }, + /** + * Unregister a timer with ID `timerID`. The unregistered timer is aborted + * and will not time out. + * + * @param {Object} params + * @param {integer} params.timerID + */ + unregisterTimer: function (params) { + var timerID = params.timerID; + if (this._timers[timerID]) { + this._timers[timerID].clear(); + delete this._timers[timerID]; + } + }, + +}); + +return Timers; + +}); + +odoo.define('im_livechat.legacy.mail.widget.Thread', function (require) { +"use strict"; + +var DocumentViewer = require('im_livechat.legacy.mail.DocumentViewer'); +var mailUtils = require('mail.utils'); + +var core = require('web.core'); +var time = require('web.time'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _lt = core._lt; + +var ORDER = { + ASC: 1, // visually, ascending order of message IDs (from top to bottom) + DESC: -1, // visually, descending order of message IDs (from top to bottom) +}; + +var READ_MORE = _lt("read more"); +var READ_LESS = _lt("read less"); + +/** + * This is a generic widget to render a thread. + * Any thread that extends mail.model.AbstractThread can be used with this + * widget. + */ +var ThreadWidget = Widget.extend({ + className: 'o_mail_thread', + + events: { + 'click a': '_onClickRedirect', + 'click img': '_onClickRedirect', + 'click strong': '_onClickRedirect', + 'click .o_thread_show_more': '_onClickShowMore', + 'click .o_attachment_download': '_onAttachmentDownload', + 'click .o_attachment_view': '_onAttachmentView', + 'click .o_attachment_delete_cross': '_onDeleteAttachment', + 'click .o_thread_message_needaction': '_onClickMessageNeedaction', + 'click .o_thread_message_star': '_onClickMessageStar', + 'click .o_thread_message_reply': '_onClickMessageReply', + 'click .oe_mail_expand': '_onClickMailExpand', + 'click .o_thread_message': '_onClickMessage', + 'click': '_onClick', + 'click .o_thread_message_notification_error': '_onClickMessageNotificationError', + 'click .o_thread_message_moderation': '_onClickMessageModeration', + 'change .moderation_checkbox': '_onChangeModerationCheckbox', + }, + + /** + * @override + * @param {widget} parent + * @param {Object} options + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.attachments = []; + // options when the thread is enabled (e.g. can send message, + // interact on messages, etc.) + this._enabledOptions = _.defaults(options || {}, { + displayOrder: ORDER.ASC, + displayMarkAsRead: true, + displayModerationCommands: false, + displayStars: true, + displayDocumentLinks: true, + displayAvatars: true, + squashCloseMessages: true, + displayNotificationIcons: true, + displayReplyIcons: false, + loadMoreOnScroll: false, + hasMessageAttachmentDeletable: false, + }); + // options when the thread is disabled + this._disabledOptions = { + displayOrder: this._enabledOptions.displayOrder, + displayMarkAsRead: false, + displayModerationCommands: false, + displayStars: false, + displayDocumentLinks: false, + displayAvatars: this._enabledOptions.displayAvatars, + squashCloseMessages: false, + displayNotificationIcons: false, + displayReplyIcons: false, + loadMoreOnScroll: this._enabledOptions.loadMoreOnScroll, + hasMessageAttachmentDeletable: false, + }; + this._selectedMessageID = null; + this._currentThreadID = null; + this._messageMailPopover = null; + this._messageSeenPopover = null; + // used to track popover IDs to destroy on re-rendering of popovers + this._openedSeenPopoverIDs = []; + }, + /** + * The message mail popover may still be shown at this moment. If we do not + * remove it, it stays visible on the page until a page reload. + * + * @override + */ + destroy: function () { + clearInterval(this._updateTimestampsInterval); + if (this._messageMailPopover) { + this._messageMailPopover.popover('hide'); + } + if (this._messageSeenPopover) { + this._messageSeenPopover.popover('hide'); + } + this._destroyOpenSeenPopoverIDs(); + this._super(); + }, + /** + * @param {im_livechat.legacy.mail.model.AbstractThread} thread the thread to render. + * @param {Object} [options] + * @param {integer} [options.displayOrder=ORDER.ASC] order of displaying + * messages in the thread: + * - ORDER.ASC: last message is at the bottom of the thread + * - ORDER.DESC: last message is at the top of the thread + * @param {boolean} [options.displayLoadMore] + * @param {Array} [options.domain=[]] the domain for the messages in the + * thread. + * @param {boolean} [options.isCreateMode] + * @param {boolean} [options.scrollToBottom=false] + * @param {boolean} [options.squashCloseMessages] + */ + render: function (thread, options) { + var self = this; + + var shouldScrollToBottomAfterRendering = false; + if (this._currentThreadID === thread.getID() && this.isAtBottom()) { + shouldScrollToBottomAfterRendering = true; + } + this._currentThreadID = thread.getID(); + + // copy so that reverse do not alter order in the thread object + var messages = _.clone(thread.getMessages({ domain: options.domain || [] })); + + var modeOptions = options.isCreateMode ? this._disabledOptions : + this._enabledOptions; + + // attachments ordered by messages order (increasing ID) + this.attachments = _.uniq(_.flatten(_.map(messages, function (message) { + return message.getAttachments(); + }))); + + options = _.extend({}, modeOptions, options, { + selectedMessageID: this._selectedMessageID, + }); + + // dict where key is message ID, and value is whether it should display + // the author of message or not visually + var displayAuthorMessages = {}; + + // Hide avatar and info of a message if that message and the previous + // one are both comments wrote by the same author at the same minute + // and in the same document (users can now post message in documents + // directly from a channel that follows it) + var prevMessage; + _.each(messages, function (message) { + if ( + // is first message of thread + !prevMessage || + // more than 1 min. elasped + (Math.abs(message.getDate().diff(prevMessage.getDate())) > 60000) || + prevMessage.getType() !== 'comment' || + message.getType() !== 'comment' || + // from a different author + (prevMessage.getAuthorID() !== message.getAuthorID()) || + ( + // messages are linked to a document thread + ( + prevMessage.isLinkedToDocumentThread() && + message.isLinkedToDocumentThread() + ) && + ( + // are from different documents + prevMessage.getDocumentModel() !== message.getDocumentModel() || + prevMessage.getDocumentID() !== message.getDocumentID() + ) + ) + ) { + displayAuthorMessages[message.getID()] = true; + } else { + displayAuthorMessages[message.getID()] = !options.squashCloseMessages; + } + prevMessage = message; + }); + + if (modeOptions.displayOrder === ORDER.DESC) { + messages.reverse(); + } + + this.$el.html(QWeb.render('im_livechat.legacy.mail.widget.Thread', { + thread: thread, + displayAuthorMessages: displayAuthorMessages, + options: options, + ORDER: ORDER, + dateFormat: time.getLangDatetimeFormat(), + })); + + _.each(messages, function (message) { + var $message = self.$('.o_thread_message[data-message-id="' + message.getID() + '"]'); + $message.find('.o_mail_timestamp').data('date', message.getDate()); + + self._insertReadMore($message); + }); + + if (shouldScrollToBottomAfterRendering) { + this.scrollToBottom(); + } + + if (!this._updateTimestampsInterval) { + this.updateTimestampsInterval = setInterval(function () { + self._updateTimestamps(); + }, 1000 * 60); + } + + this._renderMessageNotificationPopover(messages); + if (thread.hasSeenFeature()) { + this._renderMessageSeenPopover(thread, messages); + } + }, + + /** + * Render thread widget when loading, i.e. when messaging is not yet ready. + * @see /mail/init_messaging + */ + renderLoading: function () { + this.$el.html(QWeb.render('im_livechat.legacy.mail.widget.ThreadLoading')); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + getScrolltop: function () { + return this.$el.scrollTop(); + }, + /** + * State whether the bottom of the thread is visible or not, + * with a tolerance of 5 pixels + * + * @return {boolean} + */ + isAtBottom: function () { + var fullHeight = this.el.scrollHeight; + var topHiddenHeight = this.$el.scrollTop(); + var visibleHeight = this.$el.outerHeight(); + var bottomHiddenHeight = fullHeight - topHiddenHeight - visibleHeight; + return bottomHiddenHeight < 5; + }, + /** + * Removes a message and re-renders the thread + * + * @param {integer} [messageID] the id of the removed message + * @param {mail.model.AbstractThread} thread the thread which contains + * updated list of messages (so it does not contain any message with ID + * `messageID`). + * @param {Object} [options] options for the thread rendering + */ + removeMessageAndRender: function (messageID, thread, options) { + var self = this; + this._currentThreadID = thread.getID(); + return new Promise(function (resolve, reject) { + self.$('.o_thread_message[data-message-id="' + messageID + '"]') + .fadeOut({ + done: function () { + if (self._currentThreadID === thread.getID()) { + self.render(thread, options); + } + resolve(); + }, + duration: 200, + }); + }); + }, + /** + * Scroll to the bottom of the thread + */ + scrollToBottom: function () { + this.$el.scrollTop(this.el.scrollHeight); + }, + /** + * Scrolls the thread to a given message + * + * @param {integer} options.msgID the ID of the message to scroll to + * @param {integer} [options.duration] + * @param {boolean} [options.onlyIfNecessary] + */ + scrollToMessage: function (options) { + var $target = this.$('.o_thread_message[data-message-id="' + options.messageID + '"]'); + if (options.onlyIfNecessary) { + var delta = $target.parent().height() - $target.height(); + var offset = delta < 0 ? + 0 : + delta - ($target.offset().top - $target.offsetParent().offset().top); + offset = - Math.min(offset, 0); + this.$el.scrollTo("+=" + offset + "px", options.duration); + } else if ($target.length) { + this.$el.scrollTo($target); + } + }, + /** + * Scroll to the specific position in pixel + * + * If no position is provided, scroll to the bottom of the thread + * + * @param {integer} [position] distance from top to position in pixels. + * If not provided, scroll to the bottom. + */ + scrollToPosition: function (position) { + if (position) { + this.$el.scrollTop(position); + } else { + this.scrollToBottom(); + } + }, + /** + * Toggle all the moderation checkboxes in the thread + * + * @param {boolean} checked if true, check the boxes, + * otherwise uncheck them. + */ + toggleModerationCheckboxes: function (checked) { + this.$('.moderation_checkbox').prop('checked', checked); + }, + /** + * Unselect the selected message + */ + unselectMessage: function () { + this.$('.o_thread_message').removeClass('o_thread_selected_message'); + this._selectedMessageID = null; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _destroyOpenSeenPopoverIDs: function () { + _.each(this._openedSeenPopoverIDs, function (popoverID) { + $('#' + popoverID).remove(); + }); + this._openedSeenPopoverIDs = []; + }, + /** + * Modifies $element to add the 'read more/read less' functionality + * All element nodes with 'data-o-mail-quote' attribute are concerned. + * All text nodes after a ``#stopSpelling`` element are concerned. + * Those text nodes need to be wrapped in a span (toggle functionality). + * All consecutive elements are joined in one 'read more/read less'. + * + * @private + * @param {jQuery} $element + */ + _insertReadMore: function ($element) { + var self = this; + + var groups = []; + var readMoreNodes; + + // nodeType 1: element_node + // nodeType 3: text_node + var $children = $element.contents() + .filter(function () { + return this.nodeType === 1 || + this.nodeType === 3 && + this.nodeValue.trim(); + }); + + _.each($children, function (child) { + var $child = $(child); + + // Hide Text nodes if "stopSpelling" + if ( + child.nodeType === 3 && + $child.prevAll('[id*="stopSpelling"]').length > 0 + ) { + // Convert Text nodes to Element nodes + $child = $('', { + text: child.textContent, + 'data-o-mail-quote': '1', + }); + child.parentNode.replaceChild($child[0], child); + } + + // Create array for each 'read more' with nodes to toggle + if ( + $child.attr('data-o-mail-quote') || + ( + $child.get(0).nodeName === 'BR' && + $child.prev('[data-o-mail-quote="1"]').length > 0 + ) + ) { + if (!readMoreNodes) { + readMoreNodes = []; + groups.push(readMoreNodes); + } + $child.hide(); + readMoreNodes.push($child); + } else { + readMoreNodes = undefined; + self._insertReadMore($child); + } + }); + + _.each(groups, function (group) { + // Insert link just before the first node + var $readMore = $('', { + class: 'o_mail_read_more', + href: '#', + text: READ_MORE, + }).insertBefore(group[0]); + + // Toggle All next nodes + var isReadMore = true; + $readMore.click(function (e) { + e.preventDefault(); + isReadMore = !isReadMore; + _.each(group, function ($child) { + $child.hide(); + $child.toggle(!isReadMore); + }); + $readMore.text(isReadMore ? READ_MORE : READ_LESS); + }); + }); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onDeleteAttachment: function (ev) { + ev.stopPropagation(); + var $target = $(ev.currentTarget); + this.trigger_up('delete_attachment', { + attachmentId: $target.data('id'), + attachmentName: $target.data('name') + }); + }, + /** + * @private + * @param {Object} options + * @param {integer} [options.channelID] + * @param {string} options.model + * @param {integer} options.id + */ + _redirect: _.debounce(function (options) { + if ('channelID' in options) { + this.trigger('redirect_to_channel', options.channelID); + } else { + this.trigger('redirect', options.model, options.id); + } + }, 500, true), + /** + * Render the popover when mouse-hovering on the notification icon of a + * message in the thread. + * There is at most one such popover at any given time. + * + * @private + * @param {im_livechat.legacy.mail.model.AbstractMessage[]} messages list of messages in the + * rendered thread, for which popover on mouseover interaction is + * permitted. + */ + _renderMessageNotificationPopover(messages) { + if (this._messageMailPopover) { + this._messageMailPopover.popover('hide'); + } + if (!this.$('.o_thread_tooltip').length) { + return; + } + this._messageMailPopover = this.$('.o_thread_tooltip').popover({ + html: true, + boundary: 'viewport', + placement: 'auto', + trigger: 'hover', + offset: '0, 1', + content: function () { + var messageID = $(this).data('message-id'); + var message = _.find(messages, function (message) { + return message.getID() === messageID; + }); + return QWeb.render('im_livechat.legacy.mail.widget.Thread.Message.MailTooltip', { + notifications: message.getNotifications(), + }); + }, + }); + }, + /** + * Render the popover when mouse hovering on the seen icon of a message + * in the thread. Only seen icons in non-squashed message have popover, + * because squashed messages hides this icon on message mouseover. + * + * @private + * @param {im_livechat.legacy.mail.model.AbstractThread} thread with thread seen mixin, + * @see {im_livechat.legacy.mail.model.ThreadSeenMixin} + * @param {im_livechat.legacy.mail.model.Message[]} messages list of messages in the + * rendered thread. + */ + _renderMessageSeenPopover: function (thread, messages) { + var self = this; + this._destroyOpenSeenPopoverIDs(); + if (this._messageSeenPopover) { + this._messageSeenPopover.popover('hide'); + } + if (!this.$('.o_thread_message_core .o_mail_thread_message_seen_icon').length) { + return; + } + this._messageSeenPopover = this.$('.o_thread_message_core .o_mail_thread_message_seen_icon').popover({ + html: true, + boundary: 'viewport', + placement: 'auto', + trigger: 'hover', + offset: '0, 1', + content: function () { + var $this = $(this); + self._openedSeenPopoverIDs.push($this.attr('aria-describedby')); + var messageID = $this.data('message-id'); + var message = _.find(messages, function (message) { + return message.getID() === messageID; + }); + return QWeb.render('im_livechat.legacy.mail.widget.Thread.Message.SeenIconPopoverContent', { + thread: thread, + message: message, + }); + }, + }); + }, + /** + * @private + */ + _updateTimestamps: function () { + var isAtBottom = this.isAtBottom(); + this.$('.o_mail_timestamp').each(function () { + var date = $(this).data('date'); + $(this).html(mailUtils.timeFromNow(date)); + }); + if (isAtBottom && !this.isAtBottom()) { + this.scrollToBottom(); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} event + */ + _onAttachmentDownload: function (event) { + event.stopPropagation(); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onAttachmentView: function (event) { + event.stopPropagation(); + var activeAttachmentID = $(event.currentTarget).data('id'); + if (activeAttachmentID) { + var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID); + attachmentViewer.appendTo($('body')); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onChangeModerationCheckbox: function (ev) { + this.trigger_up('update_moderation_buttons'); + }, + /** + * @private + */ + _onClick: function () { + if (this._selectedMessageID) { + this.unselectMessage(); + this.trigger('unselect_message'); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMailExpand: function (ev) { + ev.preventDefault(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessage: function (ev) { + $(ev.currentTarget).toggleClass('o_thread_selected_message'); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageNeedaction: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.trigger('mark_as_read', messageID); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageNotificationError(ev) { + const messageID = $(ev.currentTarget).data('message-id'); + this.do_action('mail.mail_resend_message_action', { + additional_context: { + mail_message_to_resend: messageID, + } + }); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageReply: function (ev) { + this._selectedMessageID = $(ev.currentTarget).data('message-id'); + this.$('.o_thread_message').removeClass('o_thread_selected_message'); + this.$('.o_thread_message[data-message-id="' + this._selectedMessageID + '"]') + .addClass('o_thread_selected_message'); + this.trigger('select_message', this._selectedMessageID); + ev.stopPropagation(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageStar: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.trigger('toggle_star_status', messageID); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickMessageModeration: function (ev) { + var $button = $(ev.currentTarget); + var messageID = $button.data('message-id'); + var decision = $button.data('decision'); + this.trigger_up('message_moderation', { + messageID: messageID, + decision: decision, + }); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onClickRedirect: function (ev) { + // ignore inherited branding + if ($(ev.target).data('oe-field') !== undefined) { + return; + } + var id = $(ev.target).data('oe-id'); + if (id) { + ev.preventDefault(); + var model = $(ev.target).data('oe-model'); + var options; + if (model && (model !== 'mail.channel')) { + options = { + model: model, + id: id + }; + } else { + options = { channelID: id }; + } + this._redirect(options); + } + }, + /** + * @private + */ + _onClickShowMore: function () { + this.trigger('load_more_messages'); + }, +}); + +ThreadWidget.ORDER = ORDER; + +return ThreadWidget; + +}); + +odoo.define('im_livechat.legacy.mail.DocumentViewer', function (require) { +"use strict"; + +var core = require('web.core'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; + +var SCROLL_ZOOM_STEP = 0.1; +var ZOOM_STEP = 0.5; + +var DocumentViewer = Widget.extend({ + template: "im_livechat.legacy.mail.DocumentViewer", + events: { + 'click .o_download_btn': '_onDownload', + 'click .o_viewer_img': '_onImageClicked', + 'click .o_viewer_video': '_onVideoClicked', + 'click .move_next': '_onNext', + 'click .move_previous': '_onPrevious', + 'click .o_rotate': '_onRotate', + 'click .o_zoom_in': '_onZoomIn', + 'click .o_zoom_out': '_onZoomOut', + 'click .o_zoom_reset': '_onZoomReset', + 'click .o_close_btn, .o_viewer_img_wrapper': '_onClose', + 'click .o_print_btn': '_onPrint', + 'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox + 'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE + 'keydown': '_onKeydown', + 'keyup': '_onKeyUp', + 'mousedown .o_viewer_img': '_onStartDrag', + 'mousemove .o_viewer_content': '_onDrag', + 'mouseup .o_viewer_content': '_onEndDrag' + }, + /** + * The documentViewer takes an array of objects describing attachments in + * argument, and the ID of an active attachment (the one to display first). + * Documents that are not of type image or video are filtered out. + * + * @override + * @param {Array} attachments list of attachments + * @param {integer} activeAttachmentID + */ + init: function (parent, attachments, activeAttachmentID) { + this._super.apply(this, arguments); + this.attachment = _.filter(attachments, function (attachment) { + var match = attachment.type === 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : attachment.mimetype.match("(image|video|application/pdf|text)"); + if (match) { + attachment.fileType = match[1]; + if (match[1].match("(.png|.jpg|.gif)")) { + attachment.fileType = 'image'; + } + if (match[1] === 'youtu') { + var youtube_array = attachment.url.split('/'); + var youtube_token = youtube_array[youtube_array.length - 1]; + if (youtube_token.indexOf('watch') !== -1) { + youtube_token = youtube_token.split('v=')[1]; + var amp = youtube_token.indexOf('&'); + if (amp !== -1) { + youtube_token = youtube_token.substring(0, amp); + } + } + attachment.youtube = youtube_token; + } + return true; + } + }); + this.activeAttachment = _.findWhere(attachments, { id: activeAttachmentID }); + this.modelName = 'ir.attachment'; + this._reset(); + }, + /** + * Open a modal displaying the active attachment + * @override + */ + start: function () { + this.$el.modal('show'); + this.$el.on('hidden.bs.modal', _.bind(this._onDestroy, this)); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); + this.$('[data-toggle="tooltip"]').tooltip({ delay: 0 }); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + if (this.isDestroyed()) { + return; + } + this.trigger_up('document_viewer_closed'); + this.$el.modal('hide'); + this.$el.remove(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------------- + + /** + * @private + */ + _next: function () { + var index = _.findIndex(this.attachment, this.activeAttachment); + index = (index + 1) % this.attachment.length; + this.activeAttachment = this.attachment[index]; + this._updateContent(); + }, + /** + * @private + */ + _previous: function () { + var index = _.findIndex(this.attachment, this.activeAttachment); + index = index === 0 ? this.attachment.length - 1 : index - 1; + this.activeAttachment = this.attachment[index]; + this._updateContent(); + }, + /** + * @private + */ + _reset: function () { + this.scale = 1; + this.dragStartX = this.dragstopX = 0; + this.dragStartY = this.dragstopY = 0; + }, + /** + * Render the active attachment + * + * @private + */ + _updateContent: function () { + this.$('.o_viewer_content').html(QWeb.render('im_livechat.legacy.mail.DocumentViewer.Content', { + widget: this + })); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); + this.$('[data-toggle="tooltip"]').tooltip({ delay: 0 }); + this._reset(); + }, + /** + * Get CSS transform property based on scale and angle + * + * @private + * @param {float} scale + * @param {float} angle + */ + _getTransform: function (scale, angle) { + return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'; + }, + /** + * Rotate image clockwise by provided angle + * + * @private + * @param {float} angle + */ + _rotate: function (angle) { + this._reset(); + var new_angle = (this.angle || 0) + angle; + this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle)); + this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%'); + this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%'); + this.angle = new_angle; + }, + /** + * Zoom in/out image by provided scale + * + * @private + * @param {integer} scale + */ + _zoom: function (scale) { + if (scale > 0.5) { + this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0)); + this.scale = scale; + } + this.$('.o_zoom_reset').add('.o_zoom_out').toggleClass('disabled', scale === 1); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} e + */ + _onClose: function (e) { + e.preventDefault(); + this.destroy(); + }, + /** + * When popup close complete destroyed modal even DOM footprint too + * + * @private + */ + _onDestroy: function () { + this.destroy(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onDownload: function (e) { + e.preventDefault(); + window.location = '/web/content/' + this.modelName + '/' + this.activeAttachment.id + '/' + 'datas' + '?download=true'; + }, + /** + * @private + * @param {MouseEvent} e + */ + _onDrag: function (e) { + e.preventDefault(); + if (this.enableDrag) { + var $image = this.$('.o_viewer_img'); + var $zoomer = this.$('.o_viewer_zoomer'); + var top = $image.prop('offsetHeight') * this.scale > $zoomer.height() ? e.clientY - this.dragStartY : 0; + var left = $image.prop('offsetWidth') * this.scale > $zoomer.width() ? e.clientX - this.dragStartX : 0; + $zoomer.css("transform", "translate3d(" + left + "px, " + top + "px, 0)"); + $image.css('cursor', 'move'); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onEndDrag: function (e) { + e.preventDefault(); + if (this.enableDrag) { + this.enableDrag = false; + this.dragstopX = e.clientX - this.dragStartX; + this.dragstopY = e.clientY - this.dragStartY; + this.$('.o_viewer_img').css('cursor', ''); + } + }, + /** + * On click of image do not close modal so stop event propagation + * + * @private + * @param {MouseEvent} e + */ + _onImageClicked: function (e) { + e.stopPropagation(); + }, + /** + * Remove loading indicator when image loaded + * @private + */ + _onImageLoaded: function () { + this.$('.o_loading_img').hide(); + }, + /** + * Move next previous attachment on keyboard right left key + * + * @private + * @param {KeyEvent} e + */ + _onKeydown: function (e) { + switch (e.which) { + case $.ui.keyCode.RIGHT: + e.preventDefault(); + this._next(); + break; + case $.ui.keyCode.LEFT: + e.preventDefault(); + this._previous(); + break; + } + }, + /** + * Close popup on ESCAPE keyup + * + * @private + * @param {KeyEvent} e + */ + _onKeyUp: function (e) { + switch (e.which) { + case $.ui.keyCode.ESCAPE: + e.preventDefault(); + this._onClose(e); + break; + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onNext: function (e) { + e.preventDefault(); + this._next(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onPrevious: function (e) { + e.preventDefault(); + this._previous(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onPrint: function (e) { + e.preventDefault(); + var src = this.$('.o_viewer_img').prop('src'); + var script = QWeb.render('im_livechat.legacy.mail.PrintImage', { + src: src + }); + var printWindow = window.open('about:blank', "_new"); + printWindow.document.open(); + printWindow.document.write(script); + printWindow.document.close(); + }, + /** + * Zoom image on scroll + * + * @private + * @param {MouseEvent} e + */ + _onScroll: function (e) { + var scale; + if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) { + scale = this.scale + SCROLL_ZOOM_STEP; + this._zoom(scale); + } else { + scale = this.scale - SCROLL_ZOOM_STEP; + this._zoom(scale); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onStartDrag: function (e) { + e.preventDefault(); + this.enableDrag = true; + this.dragStartX = e.clientX - (this.dragstopX || 0); + this.dragStartY = e.clientY - (this.dragstopY || 0); + }, + /** + * On click of video do not close modal so stop event propagation + * and provide play/pause the video instead of quitting it + * + * @private + * @param {MouseEvent} e + */ + _onVideoClicked: function (e) { + e.stopPropagation(); + var videoElement = e.target; + if (videoElement.paused) { + videoElement.play(); + } else { + videoElement.pause(); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onRotate: function (e) { + e.preventDefault(); + this._rotate(90); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomIn: function (e) { + e.preventDefault(); + var scale = this.scale + ZOOM_STEP; + this._zoom(scale); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomOut: function (e) { + e.preventDefault(); + var scale = this.scale - ZOOM_STEP; + this._zoom(scale); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomReset: function (e) { + e.preventDefault(); + this.$('.o_viewer_zoomer').css("transform", ""); + this._zoom(1); + }, +}); +return DocumentViewer; +}); diff --git a/addons/im_livechat/static/src/legacy/public_livechat.scss b/addons/im_livechat/static/src/legacy/public_livechat.scss new file mode 100644 index 00000000000..16b34c2e31d --- /dev/null +++ b/addons/im_livechat/static/src/legacy/public_livechat.scss @@ -0,0 +1,634 @@ +//------------------------------------- +// legacy: mail/abstract_thread_window.scss +//------------------------------------- + +$o-mail-thread-window-zindex: $zindex-modal + 1 !default; + +.o_thread_window { + direction: ltr; + display: flex; + flex-flow: column nowrap; + position: fixed; + width: $o-mail-thread-window-width; + max-width: 100%; + height: 400px; + max-height: 100%; + font-size: 12px; + background-color: $o-mail-thread-window-bg; + border-radius: 6px 6px 0 0; + z-index: $o-mail-thread-window-zindex; + box-shadow: -5px -5px 10px rgba(black, 0.18); + + @include media-breakpoint-down(sm) { + width: 100%; + height: 100%!important; + box-shadow: none; + &.o_folded { + display: none; + } + } + + @media print { + display: none; + } + + .o_thread_window_header { + align-items: center; + display: flex; + flex: 0 0 auto; + color: white; + padding: $o-mail-chatter-gap*0.5 $o-mail-chatter-gap; + border-radius: 3px 3px 0 0; + border-bottom: 1px solid gray('300'); + background-color: $o-brand-odoo; + padding: 8px; + + @include media-breakpoint-down(sm) { + align-items: center; + height: $o-mail-chat-header-height; + padding: 0; + border-radius: 0px; + .o_thread_window_title { + font-size: 16px; + margin-left: 10px; + } + + .o_thread_window_close { + $o-close-font-size: 17px; + padding: (($o-mail-chat-header-height - $o-close-font-size) / 2); + font-size: $o-close-font-size; + color: white; + } + } + .o_thread_window_avatar { + margin: -6px 6px -6px 0; + position: relative; + + img { + height: 25px; + width: 25px; + border-radius: 50%; + } + span { + bottom: -4px; + right: -2px; + position: absolute; + + .fa-circle-o { + display: none; + } + } + } + + .o_thread_window_title { + cursor: pointer; + flex: 1 1 auto; + @include o-text-overflow; + + .o_mail_thread_typing_icon { + padding-left: 2px; + + .o_mail_thread_typing_icon_dot { + background: gray('300'); + } + } + } + + .o_thread_window_buttons { + flex: 0 0 auto; + .o_thread_window_close, .o_thread_window_expand { + color: white; + padding: 3px; + margin-left: 5px; + @include o-hover-opacity(0.7, 1); + } + } + + } + + .o_mail_thread { + flex: 1 1 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari) + + .o_thread_date_separator { + margin: 0px 0px 15px 0px; + .o_thread_date { + background-color: $o-mail-thread-window-bg; + } + } + .o_thread_message { + padding: 4px 5px; + .o_thread_message_sidebar { + margin-right: 5px; + } + .o_attachment { + @include media-breakpoint-up(md) { + width: percentage(1/3); + } + } + } + } + + .o_thread_composer input { + width: 100%; + } +} + +.o_thread_window_dropdown { + width: auto; + height: 28px; + color: white; + background-color: gray('900'); + cursor: pointer; + box-shadow: none; + + @include media-breakpoint-down(sm) { + display: none; + } + + .o_thread_window_header { + border-radius: 0; + } + + .o_thread_window_dropdown_toggler { + padding: 5px; + + .o_total_unread_counter { + @include o-position-absolute(-10px, 0, auto, auto); + background-color: $o-brand-primary; + padding: 0 2px; + font-size: smaller; + } + } + &.show .o_thread_window_dropdown_toggler .o_total_unread_counter, .o_thread_window_expand { + display: none; + } + + > ul { + max-width: $o-mail-thread-window-width; + padding: 0; + + > li.o_thread_window_header { + font-size: 12px; + padding: 3px 5px; + &~li.o_thread_window_header { + border-top: 1px solid white; + } + &:hover { + background-color: darken($o-brand-odoo, 10%); + } + } + } +} + +.o_ui_blocked .o_thread_window { + // We cannot put the z-index of thread windows directly to be greater than + // blockUI's as ui-autocomplete dropdowns (which are below blockUI) would + // appear under the thread windows (and ui-autocomplete is used to choose the + // person you want to chat with). So we only raise the z-index value when + // the ui is really blocked (in that case, the ui-autocomplete dropdowns + // will disappear under the thread windows but this is not really an issue as + // there should not be any at that time). + z-index: 1101; // blockUI's z-index is 1100 +} + +.o_no_thread_window .o_thread_window { + display: none; +} + +//------------------------------------- +// legacy: mail/thread.scss +//------------------------------------- + + +.o_mail_thread_loading { + display: flex; + align-items: center; + justify-content: center; +} + +.o_mail_thread_loading_icon { + margin-right: 5px; +} + +.o_mail_thread, .o_mail_activity { + .o_thread_show_more { + text-align: center; + } + + .o_mail_thread_content { + display: flex; + flex-direction: column; + min-height: 100%; + } + + .o_thread_bottom_free_space { + height: 15px; + } + + .o_thread_tooltip_container { + display: inline; + position: relative; + } + + .o_thread_date_separator { + margin-top: 15px; + margin-bottom: 30px; + @include media-breakpoint-down(sm) { + margin-top: 0px; + margin-bottom: 15px; + } + border-bottom: 1px solid gray('400'); + text-align: center; + + .o_thread_date { + position: relative; + top: 10px; + margin: 0 auto; + padding: 0 10px; + font-weight: bold; + background: white; + } + } + + .o_thread_new_messages_separator { + margin-bottom: 15px; + border-bottom: solid lighten($o-brand-odoo, 15%) 1px; + text-align: right; + .o_thread_separator_label { + position: relative; + top: 8px; + padding: 0 10px; + background: white; + color: lighten($o-brand-odoo, 15%); + font-size: smaller; + } + } + + .o_thread_message { + display: flex; + padding: 4px $o-horizontal-padding; + margin-bottom: 0px; + + &.o_mail_not_discussion { + background-color: rgba(gray('300'), 0.5); + border-bottom: 1px solid gray('400'); + } + + .o_thread_message_sidebar { + flex: 0 0 $o-mail-thread-avatar-size; + margin-right: 10px; + margin-top: 2px; + text-align: center; + font-size: smaller; + .o_thread_message_sidebar_image { + position: relative; + height: $o-mail-thread-avatar-size; + + .o_updatable_im_status { + width: $o-mail-thread-avatar-size; + } + .o_mail_user_status { + position: absolute; + bottom: 0; + right: 0; + + &.fa-circle-o { + display: none; + } + } + } + + @include media-breakpoint-down(sm) { + margin-top: 4px; + font-size: x-small; + } + + .o_thread_message_avatar { + width: $o-mail-thread-avatar-size; + height: $o-mail-thread-avatar-size; + object-fit: cover; + } + .o_thread_message_side_date { + display: none; + margin-left: -5px; + } + .o_thread_message_star { + display: none; + margin-right: -5px; + } + + .o_thread_message_side_date { + opacity: 0; + } + + .o_mail_thread_message_seen_icon:not(.o_all_seen) { + opacity: $o-mail-thread-icon-opacity*0.5; + } + } + .o_thread_icon { + cursor: pointer; + opacity: 0; + &.fa-star { + opacity: $o-mail-thread-icon-opacity; + color: gold; + } + } + + &:hover, &.o_thread_selected_message { + .o_thread_message_side_date { + display: inline-block; + opacity: $o-mail-thread-side-date-opacity; + } + .o_thread_icon { + display: inline-block; + opacity: $o-mail-thread-icon-opacity; + &:hover { + opacity: 1; + } + } + .o_thread_message_sidebar { + .o_mail_thread_message_seen_icon { + display: none; + } + } + } + + .o_mail_redirect { + cursor: pointer; + } + + .o_thread_message_core { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + word-wrap: break-word; + > pre { + white-space: pre-wrap; + word-break: break-word; + text-align: justify; + } + + .o_mail_note_title { + margin-top: 9px; + } + + .o_mail_subject { + font-style: italic; + } + + .o_mail_notification { + font-style: italic; + color: gray; + } + + [summary~=o_mail_notification] { // name conflicts with channel notifications, but is odoo notification buttons to hide in chatter if present + display: none; + } + + p { + margin: 0 0 9px; // Required by the old design to override a general rule on p's + &:last-child { + margin-bottom: 0; + } + } + a { + display: inline-block; + word-break: break-all; + } + :not(.o_image_box) > img { + max-width: 100%; + height: auto; + } + + .o_mail_body_long { + display: none; + } + + .o_mail_info { + margin-bottom: 2px; + + strong { + color: $headings-color; + } + } + + .o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply, .o_thread_message_notification { + padding: 4px; + } + + .o_thread_message_notification { + color: grey; + &.o_thread_message_notification_error { + color: red; + opacity: 1; + cursor: pointer; + } + } + + .o_attachments_list, .o_attachments_previews { + &:last-child { + margin-bottom: $grid-gutter-width; + } + } + + .o_thread_tooltip_container { + display: inline; + position: relative; + } + } + } + .o_thread_title { + margin-top: 20px; + margin-bottom: 20px; + font-weight: bold; + font-size: 125%; + + &.o_neutral_face_icon:before { + @extend %o-nocontent-init-image; + @include size(120px, 140px); + background: transparent url(/web/static/src/img/neutral_face.svg) no-repeat center; + } + } + + .o_mail_no_content { + @include o-position-absolute(30%, 0, 0, 0); + text-align: center; + font-size: 115%; + } + + .o_thread_message .o_thread_message_core .o_mail_read_more { + display: block; + } +} + +.o_web_client .popover .o_thread_tooltip_icon { + min-width: 1rem; +} + +.o_web_client.o_touch_device { + .o_mail_thread .o_thread_icon { + opacity: $o-mail-thread-icon-opacity; + } +} + +// ------------------------------------------------------------------ +// Thread typing icon: shared between discuss and chat windows +// ------------------------------------------------------------------ + +.o_mail_thread_typing_icon { + position: relative; + text-align: center; + margin-left: auto; + margin-right: auto; + + .o_mail_thread_typing_icon_dot { + display: inline-block; + width: 3px; + height: 3px; + border-radius: 50%; + background: gray('800'); + animation: o_mail_thread_typing_icon_dot 1.5s linear infinite; + + &:nth-child(2) { + animation-delay: -1.35s; + } + + &:nth-child(3) { + animation-delay: -1.2s; + } + } +} + +@keyframes o_mail_thread_typing_icon_dot { + 0%, 40%, 100% { + transform: initial; + } + 20% { + transform: translateY(-5px); + } +} + +// ------------------------------------------------------------------ +// Thread seen icon: shared between discuss and chat windows +// ------------------------------------------------------------------ + +.o_mail_thread_message_seen_icon { + position: relative; + opacity: 0.6; + + &.o_all_seen { + color: $o-enterprise-color; + } + + &:hover { + cursor: pointer; + opacity: 0.8; + } + + .fa-check:nth-child(1) { + padding-left: 3px; + } + + .fa-check:nth-child(2) { + position: absolute; + top: -1px; + left: 0px; + } +} + +//------------------------------------- +// legacy: im_livechat/im_livechat.scss +//------------------------------------- + +.o_livechat_button { + position: fixed; + bottom: 0px; + right: 6px; + min-width: 100px; + cursor: pointer; + white-space: nowrap; + 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; + z-index: 5; +} + +.o_thread_window { + z-index: 1002; // to go over the navbar + .o_thread_date_separator { + display: none; + } + + .o_livechat_rating { + /* Livechat Rating : feedback smiley */ + flex: 1 1 auto; + overflow: auto; + padding: 15px; + font-size: 14px; + + .o_livechat_email { + font-size: 12px; + > div { + padding: 5px 0; + } + } + + .o_livechat_no_feedback { + text-decoration: underline; + cursor: pointer; + margin: 20px 0; + } + + .o_livechat_rating_box { + margin: 40px 0 30px 0; + } + + .o_livechat_rating_choices { + margin: 10px 0; + + > img { + width: 65px; + opacity: 0.60; + cursor: pointer; + margin: 10px; + &:hover, &.selected { + opacity: 1; + } + } + } + + /* feedback reason */ + .o_livechat_rating_reason { + margin: 10px 0; + display: none; /* hidden by default */ + + > textarea { + width: 100%; + height: 70px; + resize: none; + } + } + + .o_livechat_rating_reason_button > input { + float: right; + } + } +} + +.o_livechat_operator_avatar { + padding-right: 8px; +} + +.o_livechat_no_rating { + opacity: 0.5; +} diff --git a/addons/im_livechat/static/src/legacy/public_livechat.xml b/addons/im_livechat/static/src/legacy/public_livechat.xml new file mode 100644 index 00000000000..e9873f44f52 --- /dev/null +++ b/addons/im_livechat/static/src/legacy/public_livechat.xml @@ -0,0 +1,658 @@ + + + + + +
+
+
+ Did we correctly answer your question ? +
+
+ Good + OK + Bad +
+
+
+ +
+ +
+
+
+ Receive a copy of this conversation +
+ + +
+
+ +
+ Close conversation +
+
+
+ + + +
+
+ + + + + + +
+
+
+
+ +
+
+
+ + + + + + + + + () + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Please wait... +
+ + + +
+ + +
+
+ +
+ +
+ + Viewer + +