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/*"],