[REF] mail, im_livechat: move typing to discuss WIP
closes odoo/odoo#120851 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<t t-name="im_livechat.ThreadIcon" t-inherit="mail.ThreadIcon" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//*[contains(@class, 'o-mail-ThreadIcon')]" position="inside">
|
||||
<t t-if="props.thread.type === 'livechat'">
|
||||
<Typing t-if="props.thread.hasTypingMembers" channel="props.thread" size="props.size" displayText="false"/>
|
||||
<Typing t-if="typingService.hasTypingMembers(props.thread)" channel="props.thread" size="props.size" displayText="false"/>
|
||||
<div t-else="" class="fa fa-fw fa-comments" title="Livechat"/>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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...']"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} */
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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"/>
|
||||
<div t-if="thread and thread.type !== 'chatter' and !compact">
|
||||
<Typing channel="thread" size="'medium'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span t-if="props.composer.message" class="text-muted">escape to <a href="#" t-on-click="props.onDiscardCallback">cancel</a>, enter to <a href="#" t-on-click="editMessage">save</a></span>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Props, Env>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.Typing" owl="1">
|
||||
<div class="o-mail-Typing d-flex">
|
||||
<t t-if="props.channel.hasTypingMembers">
|
||||
<div class="o-mail-Typing-icon d-flex align-items-center" t-attf-class="{{ className }}" t-att-title="text">
|
||||
<span class="o-mail-Typing-dot d-flex flex-shrink-0 rounded-pill bg-500" t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"/>
|
||||
<span class="flex-grow-1 flex-shrink-0"/>
|
||||
<span class="o-mail-Typing-dot o-mail-Typing-dot2 d-flex flex-shrink-0 rounded-pill bg-500" t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"/>
|
||||
<span class="flex-grow-1 flex-shrink-0"/>
|
||||
<span class="o-mail-Typing-dot o-mail-Typing-dot3 d-flex flex-shrink-0 rounded-pill bg-500" t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"/>
|
||||
</div>
|
||||
<t t-if="props.displayText">
|
||||
<span class="ms-1"/>
|
||||
<span class="text-truncate" t-out="text"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.Composer" t-inherit-mode="extension">
|
||||
<xpath expr="//AttachmentList" position="after">
|
||||
<div t-if="thread and thread.type !== 'chatter' and !compact">
|
||||
<Typing channel="thread" size="'medium'"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//*[hasclass('o-mail-Composer-input')]" position="attributes">
|
||||
<attribute name="t-on-input">onInput</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,2 @@
|
||||
$o-discuss-Typing-medium: 5px !default;
|
||||
$o-discuss-Typing-small: 3px !default;
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
|
||||
<xpath expr="//t[@name='chat']" position="replace">
|
||||
<Typing t-if="typingService.hasTypingMembers(props.thread)" channel="props.thread" size="props.size" displayText="false"/>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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<Props, Env>}
|
||||
*/
|
||||
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]
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -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 */
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="discuss.Typing" owl="1">
|
||||
<div class="o-discuss-Typing d-flex">
|
||||
<t t-if="typingService.hasTypingMembers(props.channel)">
|
||||
<div class="o-discuss-Typing-icon d-flex align-items-center" t-attf-class="{{ className }}" t-att-title="text">
|
||||
<span class="o-discuss-Typing-dot d-flex flex-shrink-0 rounded-pill bg-500"
|
||||
t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"
|
||||
/>
|
||||
<span class="flex-grow-1 flex-shrink-0"/>
|
||||
<span class="o-discuss-Typing-dot o-discuss-Typing-dot2 d-flex flex-shrink-0 rounded-pill bg-500"
|
||||
t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"
|
||||
/>
|
||||
<span class="flex-grow-1 flex-shrink-0"/>
|
||||
<span class="o-discuss-Typing-dot o-discuss-Typing-dot3 d-flex flex-shrink-0 rounded-pill bg-500"
|
||||
t-att-class="{
|
||||
'o-sizeMedium': props.size === 'medium',
|
||||
'o-sizeSmall': props.size === 'small',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<t t-if="props.displayText">
|
||||
<span class="ms-1"/>
|
||||
<span class="text-truncate" t-out="text"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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<number, Set<number>>} memberIdsByChannelId
|
||||
* @property {Map<number, number>} 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
<div t-if="!props.thread.authorizedGroupFullName" class="fa fa-fw fa-globe" title="Public Channel"/>
|
||||
</t>
|
||||
<t t-elif="props.thread.type === 'chat' and chatPartner">
|
||||
<Typing t-if="props.thread.hasTypingMembers" channel="props.thread" size="props.size" displayText="false"/>
|
||||
<t t-else="">
|
||||
<t name="chat">
|
||||
<t name="chat_static">
|
||||
<div t-if="chatPartner.im_status === 'online'" class="fa fa-fw fa-circle text-success" title="Online"/>
|
||||
<div t-elif="chatPartner.im_status === 'offline'" class="fa fa-fw fa-circle-o" title="Offline"/>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
+25
-26
@@ -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(() =>
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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/*"],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user