From 7e0c68bf2b1f474426c47ea7b3597e8290a97412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Wed, 10 May 2023 17:52:31 +0000 Subject: [PATCH] [REF] mail, im_livechat: move typing to discuss WIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes odoo/odoo#120851 Signed-off-by: Alexandre Kühn (aku) --- .../src/discuss_app/thread_icon_patch.xml | 2 +- .../static/tests/composer_patch_tests.js | 4 +- .../static/tests/thread_icon_patch_tests.js | 4 +- addons/mail/__manifest__.py | 3 +- .../static/src/attachments/attachment_list.js | 14 +- .../src/attachments/attachment_model.js | 2 +- .../src/attachments/attachment_service.js | 4 +- addons/mail/static/src/composer/composer.js | 47 +------ addons/mail/static/src/composer/composer.xml | 4 - .../static/src/composer/suggestion_hook.js | 2 +- addons/mail/static/src/composer/typing.js | 52 ------- addons/mail/static/src/composer/typing.xml | 31 ----- .../static/src/core/channel_member_model.js | 3 +- .../static/src/core/channel_member_service.js | 14 +- .../mail/static/src/core/message_service.js | 4 +- .../mail/static/src/core/messaging_service.js | 33 +---- addons/mail/static/src/core/thread_model.js | 19 +-- addons/mail/static/src/core/thread_service.js | 20 ++- .../static/src/core_ui/message_in_reply.js | 2 +- .../src/core_ui/message_reaction_menu.js | 2 +- .../static/src/core_ui/primary_variables.scss | 3 - .../mail/static/src/discuss/composer_patch.js | 29 ---- .../src/discuss/typing/composer_patch.js | 82 +++++++++++ .../src/discuss/typing/composer_patch.xml | 13 ++ .../src/discuss/typing/primary_variables.scss | 2 + .../src/discuss/typing/thread_icon_patch.js | 20 +++ .../src/discuss/typing/thread_icon_patch.xml | 9 ++ .../mail/static/src/discuss/typing/typing.js | 50 +++++++ .../{composer => discuss/typing}/typing.scss | 16 +-- .../mail/static/src/discuss/typing/typing.xml | 37 +++++ .../src/discuss/typing/typing_service.js | 130 ++++++++++++++++++ .../static/src/discuss_app/thread_icon.js | 4 +- .../static/src/discuss_app/thread_icon.xml | 3 +- .../mail/static/src/public/discuss_public.js | 2 +- addons/mail/static/src/public/welcome_page.js | 2 +- .../src/scss/variables/primary_variables.scss | 2 - .../src/web/messaging_menu/messaging_menu.js | 4 +- .../typing}/typing_tests.js | 51 ++++--- .../discuss/typing/setup_manager_patch.js | 14 ++ addons/web/tooling/_jsconfig.json | 1 + 40 files changed, 453 insertions(+), 287 deletions(-) delete mode 100644 addons/mail/static/src/composer/typing.js delete mode 100644 addons/mail/static/src/composer/typing.xml delete mode 100644 addons/mail/static/src/discuss/composer_patch.js create mode 100644 addons/mail/static/src/discuss/typing/composer_patch.js create mode 100644 addons/mail/static/src/discuss/typing/composer_patch.xml create mode 100644 addons/mail/static/src/discuss/typing/primary_variables.scss create mode 100644 addons/mail/static/src/discuss/typing/thread_icon_patch.js create mode 100644 addons/mail/static/src/discuss/typing/thread_icon_patch.xml create mode 100644 addons/mail/static/src/discuss/typing/typing.js rename addons/mail/static/src/{composer => discuss/typing}/typing.scss (64%) create mode 100644 addons/mail/static/src/discuss/typing/typing.xml create mode 100644 addons/mail/static/src/discuss/typing/typing_service.js rename addons/mail/static/tests/{composer => discuss/typing}/typing_tests.js (88%) create mode 100644 addons/mail/static/tests/helpers/discuss/typing/setup_manager_patch.js diff --git a/addons/im_livechat/static/src/discuss_app/thread_icon_patch.xml b/addons/im_livechat/static/src/discuss_app/thread_icon_patch.xml index 64e0f9dc0f8..11c8041ee12 100644 --- a/addons/im_livechat/static/src/discuss_app/thread_icon_patch.xml +++ b/addons/im_livechat/static/src/discuss_app/thread_icon_patch.xml @@ -3,7 +3,7 @@ - +
diff --git a/addons/im_livechat/static/tests/composer_patch_tests.js b/addons/im_livechat/static/tests/composer_patch_tests.js index d7a542266b9..6010b26c320 100644 --- a/addons/im_livechat/static/tests/composer_patch_tests.js +++ b/addons/im_livechat/static/tests/composer_patch_tests.js @@ -81,7 +81,7 @@ QUnit.test('Receives visitor typing status "is typing"', async (assert) => { }); const { env, openDiscuss } = await start(); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); const channel = pyEnv["discuss.channel"].searchRead([["id", "=", channelId]])[0]; // simulate receive typing notification from livechat visitor "is typing" await afterNextRender(() => @@ -91,7 +91,7 @@ QUnit.test('Receives visitor typing status "is typing"', async (assert) => { uuid: channel.uuid, }) ); - assert.containsOnce($, ".o-mail-Typing:contains(Visitor 20 is typing...)"); + assert.containsOnce($, ".o-discuss-Typing:contains(Visitor 20 is typing...)"); }); QUnit.test('display canned response suggestions on typing ":"', async (assert) => { diff --git a/addons/im_livechat/static/tests/thread_icon_patch_tests.js b/addons/im_livechat/static/tests/thread_icon_patch_tests.js index 26c6c5e66cf..7511ae5c350 100644 --- a/addons/im_livechat/static/tests/thread_icon_patch_tests.js +++ b/addons/im_livechat/static/tests/thread_icon_patch_tests.js @@ -27,9 +27,9 @@ QUnit.test("Public website visitor is typing", async (assert) => { uuid: channel.uuid, }) ); - assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Typing-icon"); + assert.containsOnce($, ".o-mail-Discuss-header .o-discuss-Typing-icon"); assert.containsOnce( $, - ".o-mail-Discuss-header .o-mail-Typing-icon[title='Visitor 20 is typing...']" + ".o-mail-Discuss-header .o-discuss-Typing-icon[title='Visitor 20 is typing...']" ); }); diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 57014d155b9..01c69cf0b98 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -118,8 +118,7 @@ For more specific needs, you may also assign custom-defined actions 'application': True, 'assets': { 'web._assets_primary_variables': [ - 'mail/static/src/scss/variables/primary_variables.scss', - 'mail/static/src/core_ui/primary_variables.scss', + 'mail/static/src/**/primary_variables.scss', ], 'web.assets_backend': [ # depends on BS variables, can't be loaded in assets_primary or assets_secondary diff --git a/addons/mail/static/src/attachments/attachment_list.js b/addons/mail/static/src/attachments/attachment_list.js index 14ec5e752b0..26a1c9acecc 100644 --- a/addons/mail/static/src/attachments/attachment_list.js +++ b/addons/mail/static/src/attachments/attachment_list.js @@ -28,21 +28,21 @@ export class AttachmentList extends Component { } /** - * @return {import('@mail/attachments/attachment_model').Attachment[]} + * @return {import("@mail/attachments/attachment_model").Attachment[]} */ get nonImagesAttachments() { return this.props.attachments.filter((attachment) => !attachment.isImage); } /** - * @return {import('@mail/attachments/attachment_model').Attachment[]} + * @return {import("@mail/attachments/attachment_model").Attachment[]} */ get imagesAttachments() { return this.props.attachments.filter((attachment) => attachment.isImage); } /** - * @param {import('@mail/attachments/attachment_model').Attachment} attachment + * @param {import("@mail/attachments/attachment_model").Attachment} attachment */ getImageUrl(attachment) { const { imagesHeight } = this.props; @@ -62,14 +62,14 @@ export class AttachmentList extends Component { } /** - * @param {import('@mail/attachments/attachment_model').Attachment} attachment + * @param {import("@mail/attachments/attachment_model").Attachment} attachment */ canDownload(attachment) { return !attachment.uploading && !this.env.inComposer; } /** - * @param {import('@mail/attachments/attachment_model').Attachment} attachment + * @param {import("@mail/attachments/attachment_model").Attachment} attachment */ onClickDownload(attachment) { const downloadLink = document.createElement("a"); @@ -82,7 +82,7 @@ export class AttachmentList extends Component { } /** - * @param {import('@mail/attachments/attachment_model').Attachment} attachment + * @param {import("@mail/attachments/attachment_model").Attachment} attachment */ onClickUnlink(attachment) { if (this.env.inComposer) { @@ -96,7 +96,7 @@ export class AttachmentList extends Component { } /** - * @param {import('@mail/attachments/attachment_model').Attachment} attachment + * @param {import("@mail/attachments/attachment_model").Attachment} attachment */ onConfirmUnlink(attachment) { this.props.unlinkAttachment(attachment); diff --git a/addons/mail/static/src/attachments/attachment_model.js b/addons/mail/static/src/attachments/attachment_model.js index 2f58c027b1a..e874afb557b 100644 --- a/addons/mail/static/src/attachments/attachment_model.js +++ b/addons/mail/static/src/attachments/attachment_model.js @@ -18,7 +18,7 @@ export class Attachment { url; /** @type {boolean} */ uploading; - /** @type {import('@mail/core/message_model').Message} */ + /** @type {import("@mail/core/message_model").Message} */ message; /** @type {import("@mail/core/thread_model").Thread} */ diff --git a/addons/mail/static/src/attachments/attachment_service.js b/addons/mail/static/src/attachments/attachment_service.js index ce933ff85db..6114256d75e 100644 --- a/addons/mail/static/src/attachments/attachment_service.js +++ b/addons/mail/static/src/attachments/attachment_service.js @@ -49,8 +49,8 @@ export class AttachmentService { const threadData = Array.isArray(data.originThread) ? data.originThread[0][1] : data.originThread; - // FIXME this prevents cyclic dependencies between mail.thread and mail.message - this.env.bus.trigger("MESSAGE-SERVICE:INSERT_THREAD", { + // this prevents cyclic dependencies between mail.thread and mail.attachment + this.env.bus.trigger("mail.thread/insert", { model: threadData.model, id: threadData.id, }); diff --git a/addons/mail/static/src/composer/composer.js b/addons/mail/static/src/composer/composer.js index 37cee4f40f9..18806a3eb6e 100644 --- a/addons/mail/static/src/composer/composer.js +++ b/addons/mail/static/src/composer/composer.js @@ -12,19 +12,13 @@ import { useEmojiPicker } from "../emoji_picker/emoji_picker"; import { sprintf } from "@web/core/utils/strings"; import { escapeAndCompactTextContent } from "../utils/format.js"; import { FileUploader } from "@web/views/fields/file_handler"; -import { Typing } from "./typing"; import { NavigableList } from "@mail/composer/navigable_list"; -import { useDebounced } from "@web/core/utils/timing"; import { useSuggestion } from "@mail/composer/suggestion_hook"; -import { browser } from "@web/core/browser/browser"; import { _t } from "@web/core/l10n/translation"; import { useService } from "@web/core/utils/hooks"; import { MessageConfirmDialog } from "../core_ui/message_confirm_dialog"; -export const SHORT_TYPING = 5000; -export const LONG_TYPING = 50000; - /** * @typedef {Object} Props * @property {import("@mail/composer/composer_model").Composer} composer @@ -44,7 +38,6 @@ export class Composer extends Component { static components = { AttachmentList, FileUploader, - Typing, NavigableList, }; static defaultProps = { @@ -82,15 +75,10 @@ export class Composer extends Component { this.threadService = useService("mail.thread"); this.ref = useRef("textarea"); this.fakeTextarea = useRef("fakeTextarea"); - this.typingNotified = false; this.state = useState({ autofocus: 0, active: true, }); - this.stopTyping = useDebounced(() => { - this.notifyIsTyping(false); - this.typingNotified = false; - }, SHORT_TYPING); this.selection = useSelection({ refName: "textarea", model: this.props.composer.selection, @@ -170,17 +158,6 @@ export class Composer extends Component { }); } - onInput(ev) { - if (!this.typingNotified && ev.target.value) { - this.notifyIsTyping(); - this.typingNotified = true; - browser.setTimeout(() => { - this.typingNotified = false; - }, LONG_TYPING); - } - this.stopTyping(); - } - get placeholder() { if (this.props.placeholder) { return this.props.placeholder; @@ -434,7 +411,7 @@ export class Composer extends Component { } async sendMessage() { - return this.processMessage(async (value) => { + await this.processMessage(async (value) => { const thread = this.props.messageToReplyTo?.message?.originThread ?? this.props.composer.thread; const postData = { @@ -454,31 +431,9 @@ export class Composer extends Component { } this.suggestion?.clearRawMentions(); this.props.messageToReplyTo?.cancel(); - if (this.typingNotified) { - this.typingNotified = false; - this.notifyIsTyping(false); - } }); } - /** - * Notify the server of the current typing status - * - * @param {boolean} [is_typing=true] - */ - notifyIsTyping(is_typing = true) { - if (["chat", "channel", "group"].includes(this.thread?.type)) { - this.messaging.rpc( - "/discuss/channel/notify_typing", - { - channel_id: this.thread.id, - is_typing, - }, - { silent: true } - ); - } - } - async editMessage() { if (this.ref.el.value || this.props.composer.message.attachments.length > 0) { await this.processMessage(async (value) => diff --git a/addons/mail/static/src/composer/composer.xml b/addons/mail/static/src/composer/composer.xml index c2ef1ba1910..dbeace26b62 100644 --- a/addons/mail/static/src/composer/composer.xml +++ b/addons/mail/static/src/composer/composer.xml @@ -40,7 +40,6 @@ t-ref="textarea" style="height:40px;" t-on-keydown="onKeydown" - t-on-input="onInput" t-on-focusin="onFocusin" t-on-focusout="() => this.props.composer.isFocused = false" t-on-click="(ev) => markEventHandled(ev, 'composer.onClickTextarea')" @@ -95,9 +94,6 @@ attachments="props.composer.attachments" unlinkAttachment="attachmentUploader.unlink" imagesHeight="50"/> -
- -
escape to cancel, enter to save diff --git a/addons/mail/static/src/composer/suggestion_hook.js b/addons/mail/static/src/composer/suggestion_hook.js index aa704d9baf1..aa45119a21a 100644 --- a/addons/mail/static/src/composer/suggestion_hook.js +++ b/addons/mail/static/src/composer/suggestion_hook.js @@ -5,7 +5,7 @@ import { useService } from "@web/core/utils/hooks"; export function useSuggestion() { const comp = useComponent(); - /** @type {import('@mail/composer/suggestion_service').SuggestionService} */ + /** @type {import("@mail/composer/suggestion_service").SuggestionService} */ const suggestionService = useService("mail.suggestion"); const self = { clearRawMentions() { diff --git a/addons/mail/static/src/composer/typing.js b/addons/mail/static/src/composer/typing.js deleted file mode 100644 index e078437e0e4..00000000000 --- a/addons/mail/static/src/composer/typing.js +++ /dev/null @@ -1,52 +0,0 @@ -/* @odoo-module */ - -import { Component } from "@odoo/owl"; -import { sprintf } from "@web/core/utils/strings"; -import { useMessaging } from "../core/messaging_hook"; - -import { _t } from "@web/core/l10n/translation"; - -/** - * @typedef {Object} Props - * @property {number} channel_id - * @property {string} size - * @property {boolean} displayText - * @extends {Component} - */ -export class Typing extends Component { - static defaultProps = { - size: "small", - displayText: true, - }; - static props = ["channel", "size?", "displayText?"]; - static template = "mail.Typing"; - - setup() { - this.messaging = useMessaging(); - } - - /** @returns {boolean|string} */ - get text() { - if (this.props.channel.hasTypingMembers) { - const typingMemberNames = this.props.channel.typingMembers.map(({ persona }) => - this.props.channel.getMemberName(persona) - ); - if (typingMemberNames.length === 1) { - return sprintf(_t("%s is typing..."), typingMemberNames[0]); - } - if (typingMemberNames.length === 2) { - return sprintf( - _t("%s and %s are typing..."), - typingMemberNames[0], - typingMemberNames[1] - ); - } - return sprintf( - _t("%s, %s and more are typing..."), - typingMemberNames[0], - typingMemberNames[1] - ); - } - return false; - } -} diff --git a/addons/mail/static/src/composer/typing.xml b/addons/mail/static/src/composer/typing.xml deleted file mode 100644 index 2a84190f2a2..00000000000 --- a/addons/mail/static/src/composer/typing.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - -
- -
- - - - - -
- - - - -
-
-
- -
diff --git a/addons/mail/static/src/core/channel_member_model.js b/addons/mail/static/src/core/channel_member_model.js index 1e7aae313fd..47b445fb328 100644 --- a/addons/mail/static/src/core/channel_member_model.js +++ b/addons/mail/static/src/core/channel_member_model.js @@ -10,10 +10,11 @@ import { createLocalId } from "../utils/misc"; * @property {number} threadId */ export class ChannelMember { + /** @type {number} */ + id; personaLocalId; rtcSessionId; threadId; - typingTimer; /** @type {import("@mail/core/store_service").Store} */ _store; diff --git a/addons/mail/static/src/core/channel_member_service.js b/addons/mail/static/src/core/channel_member_service.js index f275413caa4..b1362a64f35 100644 --- a/addons/mail/static/src/core/channel_member_service.js +++ b/addons/mail/static/src/core/channel_member_service.js @@ -6,10 +6,15 @@ import { ChannelMember } from "./channel_member_model"; export class ChannelMemberService { constructor(env, { "mail.store": store, "mail.persona": personaService }) { + this.env = env; this.store = store; this.personaService = personaService; } + /** + * @param {Object|Array} data + * @returns {ChannelMember} + */ insert(data) { const memberData = Array.isArray(data) ? data[1] : data; let member = this.store.channelMembers[memberData.id]; @@ -34,10 +39,17 @@ export class ChannelMemberService { }); } member.threadId = memberData.threadId ?? member.threadId ?? memberData.channel.id; + if (!member.thread) { + // this prevents cyclic dependencies between mail.thread and discuss.channel.member + this.env.bus.trigger("mail.thread/insert", { + id: member.threadId, + model: "discuss.channel", + }); + } switch (command) { case "insert": { - if (!member.thread?.channelMembers?.includes(member)) { + if (!member.thread.channelMembers.includes(member)) { member.thread.channelMembers.push(member); } } diff --git a/addons/mail/static/src/core/message_service.js b/addons/mail/static/src/core/message_service.js index a3346a695dc..9ae2f78e571 100644 --- a/addons/mail/static/src/core/message_service.js +++ b/addons/mail/static/src/core/message_service.js @@ -207,8 +207,8 @@ export class MessageService { insert(data) { let message; if (data.res_id) { - // FIXME this prevents cyclic dependencies between mail.thread and mail.message - this.env.bus.trigger("MESSAGE-SERVICE:INSERT_THREAD", { + // this prevents cyclic dependencies between mail.thread and mail.message + this.env.bus.trigger("mail.thread/insert", { model: data.model, id: data.res_id, }); diff --git a/addons/mail/static/src/core/messaging_service.js b/addons/mail/static/src/core/messaging_service.js index cab53c98dfd..e5a673be6d3 100644 --- a/addons/mail/static/src/core/messaging_service.js +++ b/addons/mail/static/src/core/messaging_service.js @@ -6,14 +6,11 @@ import { cleanTerm } from "@mail/utils/format"; import { removeFromArray, removeFromArrayWithPredicate } from "@mail/utils/arrays"; import { LinkPreview } from "./link_preview_model"; import { CannedResponse } from "./canned_response_model"; -import { browser } from "@web/core/browser/browser"; import { sprintf } from "@web/core/utils/strings"; import { _t } from "@web/core/l10n/translation"; import { createLocalId } from "../utils/misc"; import { registry } from "@web/core/registry"; -export const OTHER_LONG_TYPING = 60000; - /** * @typedef {Messaging} Messaging */ @@ -409,34 +406,6 @@ export class Messaging { } break; } - case "discuss.channel.member/typing_status": { - const isTyping = notif.payload.isTyping; - const channel = - this.store.threads[ - createLocalId("discuss.channel", notif.payload.channel.id) - ]; - if (!channel) { - return; - } - const member = this.channelMemberService.insert(notif.payload); - if (member.persona === this.store.self) { - return; - } - if (isTyping) { - if (!channel.typingMembers.includes(member)) { - channel.typingMemberIds.push(member.id); - } - if (member.typingTimer) { - browser.clearTimeout(member.typingTimer); - } - member.typingTimer = browser.setTimeout(() => { - removeFromArray(channel.typingMemberIds, member.id); - }, OTHER_LONG_TYPING); - } else { - removeFromArray(channel.typingMemberIds, member.id); - } - break; - } case "discuss.channel/unpin": { const thread = this.store.threads[createLocalId("discuss.channel", notif.payload.id)]; @@ -500,7 +469,7 @@ export class Messaging { async _handleNotificationNewMessage(notif) { const { id, message: messageData } = notif.payload; let channel = this.store.threads[createLocalId("discuss.channel", id)]; - if (!channel) { + if (!channel || !channel.type) { const [channelData] = await this.orm.call("discuss.channel", "channel_info", [id]); channel = this.threadService.insert({ id: channelData.id, diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index c95b82f7eb1..2a7450a55a0 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -14,7 +14,7 @@ import { Deferred } from "@web/core/utils/concurrency"; * @property {{id: number}} partner * @typedef SuggestedRecipient * @property {string} email - * @property {import('@mail/core/persona_model').Persona|false} persona + * @property {import("@mail/core/persona_model").Persona|false} persona * @property {string} lang * @property {string} reason * @property {boolean} checked @@ -110,7 +110,6 @@ export class Thread { /** @type {ScrollPosition} */ scrollPosition = new ScrollPosition(); showOnlyVideo = false; - typingMemberIds = []; /** @type {import("@mail/core/store_service").Store} */ _store; /** @type {string} */ @@ -133,14 +132,6 @@ export class Thread { type: data.type, _store: store, }); - if (this.type === "channel") { - this._store.discuss.channels.threads.push(this.localId); - } else if (this.type === "chat" || this.type === "group") { - this._store.discuss.chats.threads.push(this.localId); - } - if (!this.type && !["mail.box", "discuss.channel"].includes(this.model)) { - this.type = "chatter"; - } store.threads[this.localId] = this; } @@ -409,10 +400,6 @@ export class Thread { return this.memberCount - this.channelMembers.length; } - get hasTypingMembers() { - return this.typingMembers.length !== 0; - } - get rtcInvitingSession() { return this._store.rtcSessions[this.invitingRtcSessionId]; } @@ -421,10 +408,6 @@ export class Thread { return this.needactionMessages.length > 0; } - get typingMembers() { - return this.typingMemberIds.map((memberId) => this._store.channelMembers[memberId]); - } - get videoCount() { return Object.values(this.rtcSessions).filter((session) => session.videoStream).length; } diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index b76676fe150..884878a93cc 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -43,8 +43,8 @@ export class ThreadService { this.personaService = services["mail.persona"]; /** @type {import("@mail/core/message_service").MessageService} */ this.messageService = services["mail.message"]; - // FIXME this prevents cyclic dependencies between mail.thread and mail.message - this.env.bus.addEventListener("MESSAGE-SERVICE:INSERT_THREAD", ({ detail }) => { + // this prevents cyclic dependencies between mail.thread and other services + this.env.bus.addEventListener("mail.thread/insert", ({ detail }) => { const model = detail.model; const id = detail.id; const type = detail.type; @@ -812,6 +812,20 @@ export class ThreadService { ); } } + if ( + thread.type === "channel" && + !this.store.discuss.channels.threads.includes(thread.localId) + ) { + this.store.discuss.channels.threads.push(thread.localId); + } else if ( + (thread.type === "chat" || thread.type === "group") && + !this.store.discuss.chats.threads.includes(thread.localId) + ) { + this.store.discuss.chats.threads.push(thread.localId); + } + if (!thread.type && !["mail.box", "discuss.channel"].includes(thread.model)) { + thread.type = "chatter"; + } } /** @@ -1046,7 +1060,7 @@ export class ThreadService { } /** - * @param {import('@mail/core/persona_model').Persona} persona + * @param {import("@mail/core/persona_model").Persona} persona * @param {import("@mail/core/thread_model").Thread} [thread] */ avatarUrl(persona, thread) { diff --git a/addons/mail/static/src/core_ui/message_in_reply.js b/addons/mail/static/src/core_ui/message_in_reply.js index f63f4c12766..45372ba28b7 100644 --- a/addons/mail/static/src/core_ui/message_in_reply.js +++ b/addons/mail/static/src/core_ui/message_in_reply.js @@ -15,7 +15,7 @@ export class MessageInReply extends Component { this.messaging = useMessaging(); this.store = useStore(); this.user = useService("user"); - /** @type {import('@mail/core/thread_service').ThreadService} */ + /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useService("mail.thread"); } diff --git a/addons/mail/static/src/core_ui/message_reaction_menu.js b/addons/mail/static/src/core_ui/message_reaction_menu.js index 990e8e74307..a7712f7732a 100644 --- a/addons/mail/static/src/core_ui/message_reaction_menu.js +++ b/addons/mail/static/src/core_ui/message_reaction_menu.js @@ -20,7 +20,7 @@ export class MessageReactionMenu extends Component { static template = "mail.MessageReactionMenu"; setup() { - /** @type {import('@mail/core/thread_service').ThreadService} */ + /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useService("mail.thread"); this.root = useRef("root"); this.store = useStore(); diff --git a/addons/mail/static/src/core_ui/primary_variables.scss b/addons/mail/static/src/core_ui/primary_variables.scss index acf3ca3595a..31310c0f23c 100644 --- a/addons/mail/static/src/core_ui/primary_variables.scss +++ b/addons/mail/static/src/core_ui/primary_variables.scss @@ -9,9 +9,6 @@ $o-mail-LinkPreview-width: 320px !default; $o-mail-LinkPreview-height: 240px !default; $o-mail-LinkPreviewCard-height: 80px !default; -$o-mail-Typing-medium: 5px !default; -$o-mail-Typing-small: 3px !default; - $o-mail-Message-sidebarWidth: 48px !default; $o-mail-NavigableList-zIndex: 11; $o-mail-Chatter-minWidth: 530px !default; diff --git a/addons/mail/static/src/discuss/composer_patch.js b/addons/mail/static/src/discuss/composer_patch.js deleted file mode 100644 index cfb47839b3d..00000000000 --- a/addons/mail/static/src/discuss/composer_patch.js +++ /dev/null @@ -1,29 +0,0 @@ -/* @odoo-module */ - -import { Composer } from "@mail/composer/composer"; -import { registry } from "@web/core/registry"; -import { patch } from "@web/core/utils/patch"; - -const commandRegistry = registry.category("discuss.channel_commands"); - -patch(Composer.prototype, "discuss", { - /** - * @override - */ - onInput(ev) { - if (this.thread?.model === "discuss.channel" && ev.target.value.startsWith("/")) { - const [firstWord] = ev.target.value.substring(1).split(/\s/); - const command = commandRegistry.get(firstWord, false); - if ( - ev.target.value === "/" || // suggestions not yet started - this.hasSuggestions || - (command && - (!command.channel_types || command.channel_types.includes(this.thread.type))) - ) { - this.stopTyping(); - return; - } - } - this._super(ev); - }, -}); diff --git a/addons/mail/static/src/discuss/typing/composer_patch.js b/addons/mail/static/src/discuss/typing/composer_patch.js new file mode 100644 index 00000000000..b9023320dc5 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/composer_patch.js @@ -0,0 +1,82 @@ +/* @odoo-module */ + +import { Composer } from "@mail/composer/composer"; +import { Typing } from "@mail/discuss/typing/typing"; +import { browser } from "@web/core/browser/browser"; +import { patch } from "@web/core/utils/patch"; +import { registry } from "@web/core/registry"; +import { useDebounced } from "@web/core/utils/timing"; + +const commandRegistry = registry.category("discuss.channel_commands"); + +export const SHORT_TYPING = 5000; +export const LONG_TYPING = 50000; + +patch(Composer, "discuss/typing", { + components: { ...Composer.components, Typing }, +}); + +patch(Composer.prototype, "discuss/typing", { + /** + * @override + */ + setup() { + this._super(); + this.typingNotified = false; + this.stopTypingDebounced = useDebounced(this.stopTyping.bind(this), SHORT_TYPING); + }, + /** + * Notify the server of the current typing status + * + * @param {boolean} [is_typing=true] + */ + notifyIsTyping(is_typing = true) { + if (["chat", "channel", "group"].includes(this.thread?.type)) { + this.messaging.rpc( + "/discuss/channel/notify_typing", + { + channel_id: this.thread.id, + is_typing, + }, + { silent: true } + ); + } + }, + /** + * @param {InputEvent} ev + */ + onInput(ev) { + if (this.thread?.model === "discuss.channel" && ev.target.value.startsWith("/")) { + const [firstWord] = ev.target.value.substring(1).split(/\s/); + const command = commandRegistry.get(firstWord, false); + if ( + ev.target.value === "/" || // suggestions not yet started + this.hasSuggestions || + (command && + (!command.channel_types || command.channel_types.includes(this.thread.type))) + ) { + this.stopTyping(); + return; + } + } + if (!this.typingNotified && ev.target.value) { + this.typingNotified = true; + this.notifyIsTyping(); + browser.setTimeout(() => (this.typingNotified = false), LONG_TYPING); + } + this.stopTypingDebounced(); + }, + /** + * @override + */ + async sendMessage() { + await this._super(); + this.stopTyping(); + }, + stopTyping() { + if (this.typingNotified) { + this.typingNotified = false; + this.notifyIsTyping(false); + } + }, +}); diff --git a/addons/mail/static/src/discuss/typing/composer_patch.xml b/addons/mail/static/src/discuss/typing/composer_patch.xml new file mode 100644 index 00000000000..1f47fc32df6 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/composer_patch.xml @@ -0,0 +1,13 @@ + + + + +
+ +
+
+ + onInput + +
+
diff --git a/addons/mail/static/src/discuss/typing/primary_variables.scss b/addons/mail/static/src/discuss/typing/primary_variables.scss new file mode 100644 index 00000000000..7fe5730ce95 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/primary_variables.scss @@ -0,0 +1,2 @@ +$o-discuss-Typing-medium: 5px !default; +$o-discuss-Typing-small: 3px !default; diff --git a/addons/mail/static/src/discuss/typing/thread_icon_patch.js b/addons/mail/static/src/discuss/typing/thread_icon_patch.js new file mode 100644 index 00000000000..84323ddbae1 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/thread_icon_patch.js @@ -0,0 +1,20 @@ +/* @odoo-module */ + +import { ThreadIcon } from "@mail/discuss_app/thread_icon"; +import { Typing } from "@mail/discuss/typing/typing"; +import { useTypingService } from "@mail/discuss/typing/typing_service"; +import { patch } from "@web/core/utils/patch"; + +patch(ThreadIcon, "discuss/typing", { + components: { ...ThreadIcon.components, Typing }, +}); + +patch(ThreadIcon.prototype, "discuss/typing", { + /** + * @override + */ + setup() { + this._super(); + this.typingService = useTypingService(); + }, +}); diff --git a/addons/mail/static/src/discuss/typing/thread_icon_patch.xml b/addons/mail/static/src/discuss/typing/thread_icon_patch.xml new file mode 100644 index 00000000000..646a027baa0 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/thread_icon_patch.xml @@ -0,0 +1,9 @@ + + + + + + $0 + + + diff --git a/addons/mail/static/src/discuss/typing/typing.js b/addons/mail/static/src/discuss/typing/typing.js new file mode 100644 index 00000000000..234f2138456 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/typing.js @@ -0,0 +1,50 @@ +/* @odoo-module */ + +import { useMessaging } from "@mail/core/messaging_hook"; +import { useTypingService } from "@mail/discuss/typing/typing_service"; +import { Component } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; +import { sprintf } from "@web/core/utils/strings"; + +/** + * @typedef {Object} Props + * @property {import("@mail/core/thread_model").Thread} channel + * @property {string} size + * @property {boolean} displayText + * @extends {Component} + */ +export class Typing extends Component { + static defaultProps = { + size: "small", + displayText: true, + }; + static props = ["channel", "size?", "displayText?"]; + static template = "discuss.Typing"; + + setup() { + this.messaging = useMessaging(); + this.typingService = useTypingService(); + } + + /** @returns {string} */ + get text() { + const typingMemberNames = this.typingService + .getTypingMembers(this.props.channel) + .map(({ persona }) => this.props.channel.getMemberName(persona)); + if (typingMemberNames.length === 1) { + return sprintf(_t("%s is typing..."), typingMemberNames[0]); + } + if (typingMemberNames.length === 2) { + return sprintf( + _t("%s and %s are typing..."), + typingMemberNames[0], + typingMemberNames[1] + ); + } + return sprintf( + _t("%s, %s and more are typing..."), + typingMemberNames[0], + typingMemberNames[1] + ); + } +} diff --git a/addons/mail/static/src/composer/typing.scss b/addons/mail/static/src/discuss/typing/typing.scss similarity index 64% rename from addons/mail/static/src/composer/typing.scss rename to addons/mail/static/src/discuss/typing/typing.scss index 1960ccb1395..c8dcf6b1c48 100644 --- a/addons/mail/static/src/composer/typing.scss +++ b/addons/mail/static/src/discuss/typing/typing.scss @@ -1,21 +1,21 @@ -.o-mail-Typing-dot { +.o-discuss-Typing-dot { animation: o_mail_Typing_animation 1.5s linear infinite; &.o-sizeMedium { - width: $o-mail-Typing-medium; - height: $o-mail-Typing-medium; + width: $o-discuss-Typing-medium; + height: $o-discuss-Typing-medium; } &.o-sizeSmall { - width: $o-mail-Typing-small; - height: $o-mail-Typing-small; + width: $o-discuss-Typing-small; + height: $o-discuss-Typing-small; } - &.o-mail-Typing-dot2 { + &.o-discuss-Typing-dot2 { animation-delay: -1.35s; } - &.o-mail-Typing-dot3 { + &.o-discuss-Typing-dot3 { animation-delay: -1.2s; } } @@ -29,7 +29,7 @@ } } -.o-mail-Typing:before { +.o-discuss-Typing:before { // invisible character so that typing status bar has constant height, regardless of text content. content: "\200b"; /* unicode zero width space character */ } diff --git a/addons/mail/static/src/discuss/typing/typing.xml b/addons/mail/static/src/discuss/typing/typing.xml new file mode 100644 index 00000000000..ae3a9a21d12 --- /dev/null +++ b/addons/mail/static/src/discuss/typing/typing.xml @@ -0,0 +1,37 @@ + + + + +
+ +
+ + + + + +
+ + + + +
+
+
+ +
diff --git a/addons/mail/static/src/discuss/typing/typing_service.js b/addons/mail/static/src/discuss/typing/typing_service.js new file mode 100644 index 00000000000..c50bda4aeda --- /dev/null +++ b/addons/mail/static/src/discuss/typing/typing_service.js @@ -0,0 +1,130 @@ +/* @odoo-module */ + +import { reactive, useState } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +export const OTHER_LONG_TYPING = 60000; + +/** + * @typedef TypingState + * @property {Map>} memberIdsByChannelId + * @property {Map} timerByMemberId + */ +export class Typing { + busService; + /** @type {import("@mail/core/channel_member_service").ChannelMemberService} */ + channelMemberService; + /** @type {TypingState} */ + state = reactive({ + memberIdsByChannelId: new Map(), + timerByMemberId: new Map(), + }); + /** @type {import("@mail/core/store_service").Store} */ + storeService; + + constructor({ + bus_service: busService, + "discuss.channel.member": channelMemberService, + "mail.store": storeService, + }) { + Object.assign(this, { busService, channelMemberService, storeService }); + this.setup(); + } + + setup() { + this.busService.subscribe("discuss.channel.member/typing_status", (payload) => { + const member = this.channelMemberService.insert(payload); + if (payload.isTyping) { + this.addTypingMember(member); + } else { + this.removeTypingMember(member); + } + }); + this.busService.start(); + } + + /** + * @param {import("@mail/core/channel_member_model").ChannelMember} member + */ + addTypingMember(member) { + if (!this.state.memberIdsByChannelId.has(member.thread.id)) { + this.state.memberIdsByChannelId.set(member.thread.id, new Set()); + } + const memberIds = this.state.memberIdsByChannelId.get(member.thread.id); + memberIds.add(member.id); + browser.clearTimeout(this.state.timerByMemberId.get(member.id)); + this.state.timerByMemberId.set( + member.id, + browser.setTimeout(() => this.removeTypingMember(member), OTHER_LONG_TYPING) + ); + } + + /** + * @param {TypingState} state + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {import("@mail/core/channel_member_model").ChannelMember[]} + */ + getTypingMembers(state, channel) { + return [...(state.memberIdsByChannelId.get(channel.id) ?? new Set())] + .map((id) => this.channelMemberService.insert({ id })) + .filter((member) => member.persona !== this.storeService.self); + } + + /** + * @param {TypingState} state + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {boolean} + */ + hasTypingMembers(state, channel) { + return this.getTypingMembers(state, channel).length > 0; + } + + /** + * @param {import("@mail/core/channel_member_model").ChannelMember} member + */ + removeTypingMember(member) { + const memberIds = this.state.memberIdsByChannelId.get(member.thread.id); + if (memberIds) { + memberIds.delete(member.id); + if (memberIds.size === 0) { + this.state.memberIdsByChannelId.delete(member.thread.id); + } + } + browser.clearTimeout(this.state.timerByMemberId.get(member.id)); + this.state.timerByMemberId.delete(member.id); + } +} + +export const discussTypingService = { + dependencies: ["bus_service", "discuss.channel.member", "mail.store"], + start(env, services) { + return new Typing(services); + }, +}; + +registry.category("services").add("discuss.typing", discussTypingService); + +// wrapper to ensure service getters receive the reactive state of the component +export function useTypingService() { + /** @type {Typing} */ + const typingService = useService("discuss.typing"); + const state = useState(typingService.state); + return { + /** + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {import("@mail/core/channel_member_model").ChannelMember[]} + */ + getTypingMembers(channel) { + return typingService.getTypingMembers(state, channel); + }, + /** + * @param {import("@mail/core/thread_model").Thread} channel + * @returns {boolean} + */ + hasTypingMembers(channel) { + return typingService.hasTypingMembers(state, channel); + }, + }; +} diff --git a/addons/mail/static/src/discuss_app/thread_icon.js b/addons/mail/static/src/discuss_app/thread_icon.js index 7f37a66649e..31f3731ebd5 100644 --- a/addons/mail/static/src/discuss_app/thread_icon.js +++ b/addons/mail/static/src/discuss_app/thread_icon.js @@ -1,9 +1,8 @@ /* @odoo-module */ -import { Component } from "@odoo/owl"; import { useStore } from "@mail/core/messaging_hook"; -import { Typing } from "@mail/composer/typing"; import { createLocalId } from "@mail/utils/misc"; +import { Component } from "@odoo/owl"; /** * @typedef {Object} Props @@ -14,7 +13,6 @@ import { createLocalId } from "@mail/utils/misc"; */ export class ThreadIcon extends Component { static template = "mail.ThreadIcon"; - static components = { Typing }; static props = ["thread", "size?", "className?"]; static defaultProps = { size: "medium", diff --git a/addons/mail/static/src/discuss_app/thread_icon.xml b/addons/mail/static/src/discuss_app/thread_icon.xml index b9a3b42a494..10ccffff1e4 100644 --- a/addons/mail/static/src/discuss_app/thread_icon.xml +++ b/addons/mail/static/src/discuss_app/thread_icon.xml @@ -8,8 +8,7 @@
- - +
diff --git a/addons/mail/static/src/public/discuss_public.js b/addons/mail/static/src/public/discuss_public.js index 4a2677a0587..ca7f2445d02 100644 --- a/addons/mail/static/src/public/discuss_public.js +++ b/addons/mail/static/src/public/discuss_public.js @@ -14,7 +14,7 @@ export class DiscussPublic extends Component { setup() { this.messaging = useMessaging(); - /** @type {import('@mail/core/thread_service').ThreadService} */ + /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useService("mail.thread"); this.rtc = useService("mail.rtc"); this.store = useStore(); diff --git a/addons/mail/static/src/public/welcome_page.js b/addons/mail/static/src/public/welcome_page.js index f78711dc9b8..5793df2ccb5 100644 --- a/addons/mail/static/src/public/welcome_page.js +++ b/addons/mail/static/src/public/welcome_page.js @@ -13,7 +13,7 @@ export class WelcomePage extends Component { this.messaging = useMessaging(); this.store = useStore(); this.rpc = useService("rpc"); - /** @type {import('@mail/core/persona_service').PersonaService} */ + /** @type {import("@mail/core/persona_service").PersonaService} */ this.personaService = useService("mail.persona"); this.state = useState({ userName: "Guest", diff --git a/addons/mail/static/src/scss/variables/primary_variables.scss b/addons/mail/static/src/scss/variables/primary_variables.scss index 662e7755124..1d53ccdb9fe 100644 --- a/addons/mail/static/src/scss/variables/primary_variables.scss +++ b/addons/mail/static/src/scss/variables/primary_variables.scss @@ -1,7 +1,5 @@ $o-mail-Avatar-size: 36px !default; $o-mail-ChatWindow-width: 340px !default; -$o-mail-Typing-medium: 5px !default; -$o-mail-Typing-small: 3px !default; $o-mail-Chatter-mobile-gap: 2% !default; $o-mail-sidebar-icon-opacity: 0.7 !default; $o-mail-Discuss-inspector: 250px !default; diff --git a/addons/mail/static/src/web/messaging_menu/messaging_menu.js b/addons/mail/static/src/web/messaging_menu/messaging_menu.js index 563668c2026..89288e1b04b 100644 --- a/addons/mail/static/src/web/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/web/messaging_menu/messaging_menu.js @@ -26,9 +26,9 @@ export class MessagingMenu extends Component { this.store = useStore(); this.hasTouch = hasTouch; this.notification = useState(useService("mail.notification.permission")); - /** @type {import('@mail/web/chat_window/chat_window_service').ChatWindowService} */ + /** @type {import("@mail/web/chat_window/chat_window_service").ChatWindowService} */ this.chatWindowService = useState(useService("mail.chat_window")); - /** @type {import('@mail/core/thread_service').ThreadService} */ + /** @type {import("@mail/core/thread_service").ThreadService} */ this.threadService = useState(useService("mail.thread")); this.action = useService("action"); this.state = useState({ diff --git a/addons/mail/static/tests/composer/typing_tests.js b/addons/mail/static/tests/discuss/typing/typing_tests.js similarity index 88% rename from addons/mail/static/tests/composer/typing_tests.js rename to addons/mail/static/tests/discuss/typing/typing_tests.js index d7e497fa27c..bf12c5c32e5 100644 --- a/addons/mail/static/tests/composer/typing_tests.js +++ b/addons/mail/static/tests/discuss/typing/typing_tests.js @@ -1,6 +1,7 @@ -/** @odoo-module **/ +/* @odoo-module */ -import { nextTick } from "@web/../tests/helpers/utils"; +import { LONG_TYPING, SHORT_TYPING } from "@mail/discuss/typing/composer_patch"; +import { OTHER_LONG_TYPING } from "@mail/discuss/typing/typing_service"; import { afterNextRender, insertText, @@ -9,9 +10,7 @@ import { startServer, } from "@mail/../tests/helpers/test_utils"; import { Command } from "@mail/../tests/helpers/command"; - -import { LONG_TYPING, SHORT_TYPING } from "@mail/composer/composer"; -import { OTHER_LONG_TYPING } from "@mail/core/messaging_service"; +import { nextTick } from "@web/../tests/helpers/utils"; QUnit.module("typing"); @@ -27,7 +26,7 @@ QUnit.test('receive other member typing status "is typing"', async (assert) => { }); const { env, openDiscuss } = await start(); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); // simulate receive typing notification from demo await afterNextRender(() => @@ -37,7 +36,7 @@ QUnit.test('receive other member typing status "is typing"', async (assert) => { is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Demo is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Demo is typing..."); }); QUnit.test( @@ -54,7 +53,7 @@ QUnit.test( }); const { env, openDiscuss } = await start(); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); // simulate receive typing notification from demo "is typing" await afterNextRender(() => @@ -64,7 +63,7 @@ QUnit.test( is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Demo is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Demo is typing..."); // simulate receive typing notification from demo "is no longer typing" await afterNextRender(() => @@ -74,7 +73,7 @@ QUnit.test( is_typing: false, }) ); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); } ); @@ -93,7 +92,7 @@ QUnit.test( const { advanceTime, env, openDiscuss } = await start({ hasTimeControl: true }); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); // simulate receive typing notification from demo "is typing" await afterNextRender(() => @@ -103,10 +102,10 @@ QUnit.test( is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Demo is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Demo is typing..."); await afterNextRender(() => advanceTime(OTHER_LONG_TYPING)); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); } ); @@ -124,7 +123,7 @@ QUnit.test( }); const { advanceTime, env, openDiscuss } = await start({ hasTimeControl: true }); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); // simulate receive typing notification from demo "is typing" await afterNextRender(() => @@ -136,7 +135,7 @@ QUnit.test( is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Demo is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Demo is typing..."); // simulate receive typing notification from demo "is typing" again after long time. await advanceTime(LONG_TYPING); @@ -148,9 +147,9 @@ QUnit.test( await nextTick(); await advanceTime(LONG_TYPING); await nextAnimationFrame(); - assert.strictEqual($(".o-mail-Typing").text(), "Demo is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Demo is typing..."); await afterNextRender(() => advanceTime(OTHER_LONG_TYPING - LONG_TYPING)); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); } ); @@ -172,7 +171,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass }); const { env, openDiscuss } = await start(); await openDiscuss(channelId); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); // simulate receive typing notification from other 10 (is typing) await afterNextRender(() => @@ -182,7 +181,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Other 10 is typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Other 10 is typing..."); // simulate receive typing notification from other 11 (is typing) await afterNextRender(() => @@ -193,7 +192,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass }) ); assert.strictEqual( - $(".o-mail-Typing").text(), + $(".o-discuss-Typing").text(), "Other 10 and Other 11 are typing...", "Should display longer typer named first" ); @@ -206,7 +205,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass is_typing: true, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Other 10, Other 11 and more are typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Other 10, Other 11 and more are typing..."); // simulate receive typing notification from other 10 (no longer is typing) await afterNextRender(() => @@ -216,7 +215,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass is_typing: false, }) ); - assert.strictEqual($(".o-mail-Typing").text(), "Other 11 and Other 12 are typing..."); + assert.strictEqual($(".o-discuss-Typing").text(), "Other 11 and Other 12 are typing..."); // simulate receive typing notification from other 10 (is typing again) await afterNextRender(() => @@ -227,7 +226,7 @@ QUnit.test('receive several other members typing status "is typing"', async (ass }) ); assert.strictEqual( - $(".o-mail-Typing").text(), + $(".o-discuss-Typing").text(), "Other 11, Other 12 and more are typing...", "Should order by longer typer ('Other 10' just recently restarted typing)" ); @@ -317,7 +316,7 @@ QUnit.test( assert.verifySteps(["notify_typing:true"]); await nextAnimationFrame(); - assert.strictEqual($(".o-mail-Typing").text(), ""); + assert.strictEqual($(".o-discuss-Typing").text(), ""); } ); @@ -347,8 +346,8 @@ QUnit.test("chat: correspondent is typing", async (assert) => { is_typing: true, }) ); - assert.containsOnce($, ".o-mail-Typing-icon"); - assert.strictEqual($(".o-mail-Typing-icon")[0].title, "Demo is typing..."); + assert.containsOnce($, ".o-discuss-Typing-icon"); + assert.strictEqual($(".o-discuss-Typing-icon")[0].title, "Demo is typing..."); // simulate receive typing notification from demo "no longer is typing" await afterNextRender(() => diff --git a/addons/mail/static/tests/helpers/discuss/typing/setup_manager_patch.js b/addons/mail/static/tests/helpers/discuss/typing/setup_manager_patch.js new file mode 100644 index 00000000000..7db6c39f3c9 --- /dev/null +++ b/addons/mail/static/tests/helpers/discuss/typing/setup_manager_patch.js @@ -0,0 +1,14 @@ +/* @odoo-module */ + +import { discussTypingService } from "@mail/discuss/typing/typing_service"; +import { setupManager } from "@mail/../tests/helpers/webclient_setup"; +import { patch } from "@web/core/utils/patch"; + +patch(setupManager, "discuss/typing", { + setupServices() { + return { + ...this._super(...arguments), + "discuss.typing": discussTypingService, + }; + }, +}); diff --git a/addons/web/tooling/_jsconfig.json b/addons/web/tooling/_jsconfig.json index 68d876010a4..9bbd39e12ef 100644 --- a/addons/web/tooling/_jsconfig.json +++ b/addons/web/tooling/_jsconfig.json @@ -18,6 +18,7 @@ "@web_mobile/*": ["web_mobile/static/src/*"], "@web_grid/*": ["web_grid/static/src/*"], "@web_studio/*": ["web_studio/static/src/*"], + "@bus/*": ["addons/bus/static/src/*"], "@mail/*": ["addons/mail/static/src/*"], "@point_of_sale/*": ["addons/point_of_sale/static/src/*"],