[IMP] mail: avatar edit in channel and group

1. Display avatar in top bar next to channel name and description.
2. Edit icon appears on hover.
3. Clicking on the Edit icon opens file browser to upload a new avatar.
4. Certain file types are allowed.
5. Avatar can only be changed for group chat by any member of the channel.
6. For channel it can be done with admin rights.
7. Disable uploading text from file uploader in this case.

Also show avatar of channel/chat conversation in chat window.
Author avatar in message list no longer shown im status.
Chat window header color matches new systray color.

task-2684679

[FIX] mail: make chat window header match systray color

Part-of: odoo/odoo#117357
This commit is contained in:
Zelong Lin
2023-05-16 14:42:05 +02:00
committed by Alexandre Kühn
parent eb51266d11
commit ff122ac7ae
36 changed files with 309 additions and 183 deletions
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ImStatus" t-inherit-mode="extension">
<xpath expr="//span" position="inside">
<xpath expr="//*[@name='icon']" position="replace">
<i t-if="props.persona.im_status === 'leave_online'" class="fa fa-plane text-success" title="Online" role="img" aria-label="User is online"/>
<i t-if="props.persona.im_status === 'leave_away'" class="fa fa-plane text-warning" title="Idle" role="img" aria-label="User is idle"/>
<i t-if="props.persona.im_status === 'leave_offline'" class="fa fa-plane text-700" title="Out of office" role="img" aria-label="User is out of office"/>
</xpath>
<t t-else="">$0</t>
</xpath>
</t>
</templates>
@@ -1,61 +1,51 @@
/** @odoo-module **/
import { UPDATE_BUS_PRESENCE_DELAY } from "@bus/im_status_service";
import { start, startServer } from "@mail/../tests/helpers/test_utils";
import { Command } from "@mail/../tests/helpers/command";
QUnit.module("im_status");
QUnit.test("on leave & online", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_online" });
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
hasTimeControl: true,
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Online']");
});
QUnit.test("on leave & away", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_away" });
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
hasTimeControl: true,
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Idle']");
});
QUnit.test("on leave & offline", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_offline" });
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
});
const { advanceTime, afterNextRender, openDiscuss } = await start({
hasTimeControl: true,
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Out of office']");
});
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="im_livechat.Discuss" t-inherit="mail.Discuss" t-inherit-mode="extension" owl="1">
<xpath expr="//*[contains(@class, 'o-mail-Discuss-threadAvatar')]" position="attributes">
<attribute name="t-if">thread and thread.model === 'discuss.channel' and thread.type !== 'livechat'</attribute>
</xpath>
</t>
</templates>
+24 -4
View File
@@ -75,6 +75,26 @@ class BinaryController(http.Controller):
raise NotFound()
return request.env["ir.binary"]._get_stream_from(attachment_sudo).get_response(as_attachment=download)
@http.route(
"/discuss/channel/<int:channel_id>/avatar_128",
methods=["GET"],
type="http",
auth="public",
)
def discuss_channel_avatar_128(self, channel_id):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise(
request=request, channel_id=channel_id
)
domain = [("id", "=", channel_id)]
channel_sudo = channel_member_sudo.env["discuss.channel"].search(domain, limit=1)
if not channel_sudo:
raise NotFound()
return (
request.env["ir.binary"]
._get_image_stream_from(channel_sudo, field_name="avatar_128")
.get_response()
)
@http.route(
[
"/discuss/channel/<int:channel_id>/image/<int:attachment_id>",
@@ -86,11 +106,11 @@ class BinaryController(http.Controller):
)
def fetch_image(self, channel_id, attachment_id, width=0, height=0, **kwargs):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise(
request=request, channel_id=int(channel_id)
request=request, channel_id=channel_id
)
domain = [
("id", "=", int(attachment_id)),
("res_id", "=", int(channel_id)),
("id", "=", attachment_id),
("res_id", "=", channel_id),
("res_model", "=", "discuss.channel"),
]
attachment_sudo = channel_member_sudo.env["ir.attachment"].search(domain, limit=1)
@@ -98,6 +118,6 @@ class BinaryController(http.Controller):
raise NotFound()
return (
request.env["ir.binary"]
._get_image_stream_from(attachment_sudo, width=int(width), height=int(height))
._get_image_stream_from(attachment_sudo, width=width, height=height)
.get_response(as_attachment=kwargs.get("download"))
)
@@ -38,6 +38,13 @@ class ChannelController(http.Controller):
except UserError:
raise NotFound()
@http.route("/discuss/channel/update_avatar", methods=["POST"], type="json", auth="public")
def discuss_channel_avatar_update(self, channel_id, data):
channel = request.env["discuss.channel"].browse(int(channel_id)).exists()
if not channel or not data:
raise NotFound()
channel.write({"image_128": data})
@http.route("/discuss/channel/messages", methods=["POST"], type="json", auth="public")
def discuss_channel_messages(self, channel_id, before=None, after=None, limit=30, around=None):
channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise(
@@ -54,6 +54,7 @@ class Channel(models.Model):
('group', 'Group')],
string='Channel Type', required=True, default='channel', readonly=True, help="Chat is private and unique between 2 persons. Group is private among invited persons. Channel can be freely joined (depending on its configuration).")
is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat')
is_editable = fields.Boolean('Is Editable', compute='_compute_is_editable')
default_display_mode = fields.Selection(string="Default Display Mode", selection=[('video_full_screen', "Full screen video")], help="Determines how the channel will be displayed by default when opening it from its invitation link. No value means display text (no voice/video).")
description = fields.Text('Description')
image_128 = fields.Image("Image", max_width=128, max_height=128)
@@ -109,6 +110,18 @@ class Channel(models.Model):
for record in self:
record.is_chat = record.channel_type == 'chat'
@api.depends('channel_type', 'is_member')
def _compute_is_editable(self):
for channel in self:
if channel.channel_type == 'channel':
channel.is_editable = self.sudo(False).env.is_admin() or channel.create_uid.id == self.env.user.id
elif channel.channel_type == 'chat':
channel.is_editable = True
elif channel.channel_type == 'group':
channel.is_editable = channel.is_member and not self.sudo(False).env.user._is_public()
else:
channel.is_editable = False
@api.depends('channel_type', 'image_128', 'uuid')
def _compute_avatar_128(self):
for record in self:
@@ -776,6 +789,7 @@ class Channel(models.Model):
'description': channel.description,
'uuid': channel.uuid,
'state': 'open',
'is_editable': channel.is_editable,
'is_minimized': False,
'group_based_subscription': bool(channel.group_ids),
'create_uid': channel.create_uid.id,
+4 -6
View File
@@ -124,6 +124,8 @@ export class Thread {
last_interest_dt;
/** @type {number} */
lastServerMessageId;
/** @type {Boolean} */
is_editable;
constructor(store, data) {
Object.assign(this, {
@@ -264,12 +266,8 @@ export class Thread {
);
}
get isDescriptionChangeable() {
return !this._store.guest && ["channel", "group"].includes(this.type);
}
get isRenameable() {
return !this._store.guest && ["chat", "channel", "group"].includes(this.type);
get allowDescription() {
return ["channel", "group"].includes(this.type);
}
get isTransient() {
@@ -699,6 +699,7 @@ export class ThreadService {
"type",
"group_based_subscription",
"last_interest_dt",
"is_editable",
"defaultDisplayMode",
]);
if (serverData.channel && "message_unread_counter" in serverData.channel) {
+1 -1
View File
@@ -465,7 +465,7 @@ export class Message extends Component {
}
get imStatusClassName() {
let res = "position-absolute";
let res = "position-absolute bottom-0 end-0";
if (this.hasOpenChatFeature) {
res += " cursor-pointer";
}
@@ -31,7 +31,6 @@
t-att-title="authorText"
t-att-src="authorAvatarUrl"
t-att-class="{ 'o_redirect cursor-pointer': hasAuthorClickable }"/>
<ImStatus t-if="message.author" persona="message.author" className="imStatusClassName"/>
</div>
<t t-elif="!message.isTransient">
<small t-if="isActive" class="o-mail-Message-date text-muted opacity-50 ms-2">
@@ -2,15 +2,15 @@
<templates xml:space="preserve">
<t t-inherit="mail.ChatWindow" t-inherit-mode="extension">
<xpath expr="//*[@t-ref='startCall']" position="after">
<div t-if="env.isSmall and thread and thread.type === 'channel'" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" t-att-title="state.activeMode === 'add-users' ? 'Stop adding users' : 'Add users'" t-on-click.stop="toggleAddUsers">
<div t-if="env.isSmall and thread and thread.type === 'channel'" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" t-att-title="state.activeMode === 'add-users' ? 'Stop adding users' : 'Add users'" t-on-click.stop="toggleAddUsers">
<i class="fa fa-lg fa-user-plus"/>
</div>
</xpath>
<xpath expr="//*[@t-ref='expand']" position="before">
<div t-if="thread and thread.hasMemberList and !props.chatWindow.hidden and !props.chatWindow.folded" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" t-att-title="state.activeMode === 'member-list' ? 'Hide Member List' : 'Show Member List'" t-on-click.stop="toggleMemberList">
<div t-if="thread and thread.hasMemberList and !props.chatWindow.hidden and !props.chatWindow.folded" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" t-att-title="state.activeMode === 'member-list' ? 'Hide Member List' : 'Show Member List'" t-on-click.stop="toggleMemberList">
<i class="fa fa-users"/>
</div>
<div t-if="thread.allowCalls and !props.chatWindow.hidden and !props.chatWindow.folded" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" title="Show Call Settings" t-on-click.stop="toggleSettings">
<div t-if="thread.allowCalls and !props.chatWindow.hidden and !props.chatWindow.folded" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Show Call Settings" t-on-click.stop="toggleSettings">
<i class="fa fa-gear"/>
</div>
</xpath>
@@ -8,13 +8,14 @@ export class AutoresizeInput extends Component {
static template = "mail.AutoresizeInput";
static props = {
className: { type: String, optional: true },
disabled: { type: Boolean, optional: true },
enabled: { optional: true },
onValidate: { type: Function, optional: true },
placeholder: { type: String, optional: true },
value: { type: String },
};
static defaultProps = {
className: "",
enabled: true,
onValidate: () => {},
placeholder: "",
};
@@ -3,10 +3,10 @@
<t t-name="mail.AutoresizeInput" owl="1">
<input
class="o-mail-AutoresizeInput text-truncate px-2 py-1 border-1 bg-transparent border-transparent"
class="o-mail-AutoresizeInput text-truncate p-1 border-1 bg-transparent border-transparent"
t-attf-class="{{ props.className }}"
t-attf-placeholder="{{ props.placeholder }}"
t-att-disabled="props.disabled"
t-att-disabled="!props.enabled"
t-att-title="state.value"
t-model="state.value"
t-on-keydown="onKeydownInput"
@@ -1,5 +1,6 @@
/* @odoo-module */
import { ImStatus } from "./im_status";
import { AutoresizeInput } from "./autoresize_input";
import { Thread } from "../core_ui/thread";
import { ThreadIcon } from "./thread_icon";
@@ -8,6 +9,7 @@ import { useRtc } from "../rtc/rtc_hook";
import { useMessageEdition, useMessageHighlight, useMessageToReplyTo } from "@mail/utils/hooks";
import { Composer } from "../composer/composer";
import { Call } from "../rtc/call";
import { FileUploader } from "@web/views/fields/file_handler";
import {
Component,
onWillStart,
@@ -20,6 +22,7 @@ import {
} from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { url } from "@web/core/utils/urls";
import { PinnedMessagesPanel } from "./pinned_messages_panel";
export class Discuss extends Component {
@@ -30,6 +33,8 @@ export class Discuss extends Component {
Composer,
Call,
PinnedMessagesPanel,
FileUploader,
ImStatus,
};
static props = {
public: { type: Boolean, optional: true },
@@ -70,6 +75,7 @@ export class Discuss extends Component {
},
},
});
this.notification = useService("notification");
useEffect(
() => {
if (
@@ -100,6 +106,12 @@ export class Discuss extends Component {
return this.store.threads[this.store.discuss.threadLocalId];
}
get channelAvatar() {
return this.props.public
? url(`/discuss/channel/${this.thread.id}/avatar_128`)
: this.thread.imgUrl;
}
togglePinMenu() {
this.state.activeMode =
this.state.activeMode === this.MODES.PINNED_MESSAGES
@@ -107,6 +119,11 @@ export class Discuss extends Component {
: this.MODES.PINNED_MESSAGES;
}
async onFileUploaded(file) {
await this.threadService.updateAvatar(this.thread.id, file.data);
this.notification.add(_t("The avatar has been updated!"), { type: "success" });
}
async renameThread({ value: name }) {
const newName = name.trim();
if (
@@ -28,3 +28,24 @@
background-color: $gray-400;
}
}
.o-mail-Discuss-threadAvatar {
img {
height: 36px;
width: 36px;
}
a {
i {
transform: translate(-50%, -50%);
opacity: 0;
}
&:hover {
background-color: rgba($black, 0.5);
i {
opacity: 1;
}
}
}
}
+19 -5
View File
@@ -5,22 +5,36 @@
<div class="o-mail-Discuss d-flex h-100 flex-grow-1 border-top" t-att-class="{ 'flex-column align-items-center bg-view': store.isSmall }" t-ref="root">
<div t-if="store.isSmall and store.discuss.activeTab === 'mailbox'" class="w-100 border-bottom" t-call="mail.Discuss.mobileTopbar" t-ref="mobileTopbar"/>
<div t-if="thread and !(store.isSmall and ['chat', 'channel'].includes(store.discuss.activeTab))" class="o-mail-Discuss-content d-flex flex-column h-100 w-100 bg-view overflow-auto" t-ref="content">
<div class="o-mail-Discuss-header px-3 bg-100 border-bottom d-flex flex-shrink-0" style="height:42px;">
<ThreadIcon className="'me-2 align-self-center'" thread="thread"/>
<div class="o-mail-Discuss-header px-3 bg-100 border-bottom d-flex flex-shrink-0 align-items-center" style="height:46px;">
<div t-if="thread and thread.model === 'discuss.channel'" class="o-mail-Discuss-threadAvatar position-relative align-self-center align-items-center mt-2 mb-2 me-2">
<img class="rounded" t-att-src="channelAvatar" alt="Thread Image"/>
<FileUploader t-if="thread.is_editable" acceptedFileExtensions="'.bmp, .jpg, .jpeg, .png, .svg'" showUploadingText="false" multiUpload="false" onUploaded="(data) => this.onFileUploaded(data)">
<t t-set-slot="toggler">
<a href="#" class="position-absolute z-index-1 h-100 w-100 rounded start-0 bottom-0" title="Upload Avatar">
<i class="position-absolute top-50 start-50 fa fa-sm fa-pencil text-white"/>
</a>
</t>
</FileUploader>
</div>
<t t-else="">
<ThreadIcon className="'me-2 align-self-center'" thread="thread"/>
</t>
<ImStatus t-if="thread and thread.type === 'chat' and thread.correspondent" className="'me-1'" persona="thread.correspondent"/>
<div class="d-flex flex-grow-1 align-self-center align-items-center h-100 py-2">
<AutoresizeInput
className="'o-mail-Discuss-threadName lead fw-bold flex-shrink-1 text-dark'"
disabled="!thread.isRenameable"
enabled="thread.is_editable"
onValidate.bind="renameThread"
value="thread.displayName"
/>
<t t-if="thread.isDescriptionChangeable">
<t t-if="thread.allowDescription and (thread.is_editable or (!thread.is_editable and thread.description))">
<div class="flex-shrink-0 mx-2 py-2 border-start"/>
<t t-set="autogrowDescriptionPlaceholder">Add a description</t>
<AutoresizeInput
className="'o-mail-Discuss-threadDescription flex-shrink-1'"
enabled="thread.is_editable"
onValidate.bind="updateThreadDescription"
placeholder="autogrowDescriptionPlaceholder"
placeholder="thread.is_editable ? autogrowDescriptionPlaceholder : ''"
value="thread.description or ''"
/>
</t>
@@ -3,7 +3,7 @@
import { Component } from "@odoo/owl";
export class ImStatus extends Component {
static props = ["persona", "className?"];
static props = ["persona", "className?", "style?"];
static template = "mail.ImStatus";
static defaultProps = { className: "" };
static defaultProps = { className: "", style: "" };
}
@@ -1,8 +1,5 @@
.o-mail-ImStatus {
$-gap: $font-size-lg - $font-size-base;
@include o-position-absolute($right: -$-gap, $bottom: -$-gap);
width: $font-size-lg;
height: $font-size-lg;
}
@@ -2,12 +2,13 @@
<templates xml:space="preserve">
<t t-name="mail.ImStatus" owl="1">
<div class="o-mail-ImStatus position-absolute top-100 start-100 translate-middle w-50 h-50 d-flex justify-content-center flex-shrink-0 align-items-center mt-n1 ms-n1 rounded-circle small o-bg-inherit" t-att-class="props.className">
<span t-if="props.persona.im_status" class="d-flex flex-column">
<div class="o-mail-ImStatus d-flex justify-content-center flex-shrink-0 align-items-center rounded-circle o-bg-inherit" t-att-class="props.className" t-att-style="props.style">
<span t-if="props.persona.im_status" class="d-flex flex-column" name="icon">
<i t-if="props.persona.im_status === 'online'" class="fa fa-circle text-success" title="Online" role="img" aria-label="User is online"/>
<i t-if="props.persona.im_status === 'away'" class="fa fa-circle text-warning" title="Idle" role="img" aria-label="User is idle"/>
<i t-if="props.persona.im_status === 'offline'" class="fa fa-circle-o text-700" title="Offline" role="img" aria-label="User is offline"/>
<i t-if="props.persona.im_status === 'bot'" class="fa fa-heart text-success" title="Bot" role="img" aria-label="User is a bot"/>
<i t-elif="props.persona.im_status === 'away'" class="fa fa-circle text-warning" title="Idle" role="img" aria-label="User is idle"/>
<i t-elif="props.persona.im_status === 'offline'" class="fa fa-circle-o text-700" title="Offline" role="img" aria-label="User is offline"/>
<i t-elif="props.persona.im_status === 'bot'" class="fa fa-heart text-success" title="Bot" role="img" aria-label="User is a bot"/>
<i t-else="" class="fa fa-fw fa-question-circle" title="No IM status available"/>
</span>
</div>
</t>
@@ -14,9 +14,7 @@
<div t-elif="chatPartner.im_status === 'offline'" class="fa fa-fw fa-circle-o" title="Offline"/>
<div t-elif="chatPartner.im_status === 'away'" class="fa fa-fw fa-circle text-warning" title="Away"/>
<div t-elif="chatPartner.im_status === 'bot'" class="fa fa-fw fa-heart text-success" title="Bot"/>
<t t-else="" name="noImStatusCondition">
<div class="fa fa-fw fa-question-circle" title="No IM status available"/>
</t>
<div t-else="" class="fa fa-fw fa-question-circle" title="No IM status available"/>
</t>
</t>
</t>
@@ -1,3 +0,0 @@
.o-mail-ChatWindow {
--o-mail-ChatWindow-header-background-color: #{$o-gray-100};
}
@@ -10,6 +10,7 @@ import { Component, useChildSubEnv, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { localization } from "@web/core/l10n/localization";
import { ThreadIcon } from "@mail/discuss_app/thread_icon";
import { ImStatus } from "@mail/discuss_app/im_status";
import { isEventHandled } from "@mail/utils/misc";
import { ChannelSelector } from "@mail/discuss_app/channel_selector";
import { PinnedMessagesPanel } from "@mail/discuss_app/pinned_messages_panel";
@@ -28,6 +29,7 @@ export class ChatWindow extends Component {
ChannelSelector,
Composer,
ThreadIcon,
ImStatus,
PinnedMessagesPanel,
};
static props = ["chatWindow", "right?"];
@@ -1,21 +1,26 @@
.o-mail-ChatWindow {
height: 460px;
height: 480px;
width: $o-mail-ChatWindow-width;
z-index: 999; // messaging menu is dropdown (1000)
background-color: var(--o-mail-ChatWindow-header-background-color, #{$o-brand-odoo});
box-shadow: -5px -5px 10px rgba(#000000, 0.09);
outline: none;
&.o-folded {
height: 36px;
height: 48px;
}
.o-mail-ChatWindow-command:hover {
background-color: rgba(0, 0, 0, 0.1);
background-color: rgba(0, 0, 0, 0.075);
}
}
.o-mail-ChatWindow-header {
height: 36px;
color: #ffffff;
height: 48px;
.o-mail-ChatWindow-threadAvatar {
img {
height: 28px;
width: 28px;
}
}
}
@@ -13,11 +13,17 @@
t-on-keydown.stop="onKeydown"
tabindex="1"
>
<div class="o-mail-ChatWindow-header d-flex align-items-center flex-shrink-0" t-on-click="toggleFold" t-att-class="{ 'cursor-pointer': !store.isSmall, 'border-bottom': !props.chatWindow.folded }">
<div class="o-mail-ChatWindow-header d-flex align-items-center flex-shrink-0 bg-100" t-on-click="toggleFold" t-att-class="{ 'cursor-pointer': !store.isSmall, 'border-bottom': !props.chatWindow.folded }">
<div t-if="store.isSmall" class="cursor-pointer d-flex align-items-center h-100 px-3 py-0 me-2" title="Close conversation" t-on-click="() => this.close()">
<i class="oi oi-arrow-left"/>
</div>
<ThreadIcon t-if="thread" thread="thread" className="'ms-3 me-1 my-0'"/>
<div t-if="thread and thread.model === 'discuss.channel'" class="o-mail-ChatWindow-threadAvatar position-relative align-self-center align-items-center my-0" t-att-class="{ 'ms-3': !store.isSmall, 'me-1': thread.type === 'chat', 'me-2': thread.type !== 'chat' }">
<img class="rounded" t-att-src="thread.imgUrl" alt="Thread Image"/>
</div>
<t t-else="">
<ThreadIcon t-if="thread" thread="thread" className="'ms-3 me-1 my-0'"/>
</t>
<ImStatus t-if="thread and thread.type === 'chat' and thread.correspondent" className="'me-1'" persona="thread.correspondent"/>
<span class="o-mail-ChatWindow-name text-truncate" t-att-title="props.chatWindow.displayName" t-esc="props.chatWindow.displayName" t-att-class="{
'ms-3 me-1 my-0': !thread,
}"
@@ -26,18 +32,18 @@
(<t t-out="thread.message_unread_counter"/>)
</div>
<div class="flex-grow-1"/>
<div t-if="thread?.model === 'discuss.channel' and env.isSmall" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" title="Pinned messages" t-on-click.stop="togglePinMenu">
<div t-if="thread?.model === 'discuss.channel' and env.isSmall" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Pinned messages" t-on-click.stop="togglePinMenu">
<i class="fa fa-lg fa-thumb-tack"/>
</div>
<div t-if="thread and thread.allowCalls and thread !== rtc.state.channel and !props.chatWindow.hidden" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" title="Start a Call" t-on-click.stop="() => this.rtc.toggleCall(this.props.chatWindow.thread)" t-ref="startCall">
<div t-if="thread and thread.allowCalls and thread !== rtc.state.channel and !props.chatWindow.hidden" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Start a Call" t-on-click.stop="() => this.rtc.toggleCall(this.props.chatWindow.thread)" t-ref="startCall">
<i class="fa fa-phone"/>
</div>
<t t-if="thread">
<div t-if="!env.isSmall" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-50 opacity-100-hover" title="Open in Discuss" t-on-click.stop="expand" t-ref="expand">
<div t-if="!env.isSmall" class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 py-0 m-0 opacity-75 opacity-100-hover" title="Open in Discuss" t-on-click.stop="expand" t-ref="expand">
<i class="fa fa-expand"/>
</div>
</t>
<div class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 opacity-50 opacity-100-hover" title="Close chat window" t-on-click.stop="() => this.close()">
<div class="o-mail-ChatWindow-command d-flex align-items-center h-100 px-3 opacity-75 opacity-100-hover" title="Close chat window" t-on-click.stop="() => this.close()">
<i class="fa fa-close"></i>
</div>
</div>
@@ -57,7 +57,7 @@
onClick="() => notification.requestPermission()"
>
<t t-set-slot="icon">
<ImStatus persona="notificationRequest.partner"/>
<ImStatus persona="notificationRequest.partner" className="'position-absolute top-100 start-100 translate-middle mt-n1 ms-n1'"/>
</t>
</NotificationItem>
<t t-if="store.discuss.activeTab === 'all'">
@@ -89,7 +89,7 @@
onSwipeLeft="hasTouch() and threadService.canUnpin(thread) ? { action: () => this.threadService.unpin(thread), icon: 'fa-times-circle', bgColor: 'bg-danger' } : undefined"
>
<t t-set-slot="icon">
<ImStatus t-if="thread.type === 'chat'" persona="thread.correspondent"/>
<ImStatus t-if="thread.type === 'chat'" persona="thread.correspondent" className="'position-absolute top-100 start-100 translate-middle mt-n1 ms-n1'"/>
</t>
<t t-if="message" t-set-slot="body-icon">
<t t-if="message.isSelfAuthored">
@@ -199,6 +199,13 @@ patch(ThreadService.prototype, "mail/web", {
}
delete this.store.followers[follower.id];
},
async updateAvatar(threadId, data) {
return this.rpc("/discuss/channel/update_avatar", {
channel_id: threadId,
data,
});
},
});
patch(threadService, "mail/web", {
@@ -100,7 +100,7 @@ QUnit.test("chat window: basic rendering", async (assert) => {
await click(".o-mail-NotificationItem");
assert.containsOnce($, ".o-mail-ChatWindow");
assert.containsOnce($, ".o-mail-ChatWindow-header");
assert.containsOnce($(".o-mail-ChatWindow-header"), ".o-mail-ThreadIcon");
assert.containsOnce($(".o-mail-ChatWindow-header"), ".o-mail-ChatWindow-threadAvatar");
assert.containsOnce($, ".o-mail-ChatWindow-name:contains(General)");
assert.containsN($, ".o-mail-ChatWindow-command", 5);
assert.containsOnce($, ".o-mail-ChatWindow-command[title='Start a Call']");
@@ -56,7 +56,10 @@ QUnit.test("Delete starred message updates counter", async (assert) => {
QUnit.test("Thread rename", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const channelId = pyEnv["discuss.channel"].create({
create_uid: pyEnv.currentPartnerId,
name: "General",
});
const tab1 = await start({ asTab: true });
const tab2 = await start({ asTab: true });
await tab1.openDiscuss(channelId);
@@ -69,7 +72,10 @@ QUnit.test("Thread rename", async (assert) => {
QUnit.test("Thread description update", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const channelId = pyEnv["discuss.channel"].create({
create_uid: pyEnv.currentPartnerId,
name: "General",
});
const tab1 = await start({ asTab: true });
const tab2 = await start({ asTab: true });
await tab1.openDiscuss(channelId);
@@ -827,22 +827,25 @@ QUnit.test("rendering of inbox message", async (assert) => {
assert.containsOnce($message, "[title='Mark as Read']");
});
QUnit.test('Unfollow message', async function (assert) {
QUnit.test("Unfollow message", async function (assert) {
assert.expect(12);
const pyEnv = await startServer();
const currentPartnerId = pyEnv.currentPartnerId;
const [threadFollowedId, threadNotFollowedId] = pyEnv["res.partner"].create([{
name: 'Thread followed',
}, {
name: 'Thread not followed',
}]);
const [threadFollowedId, threadNotFollowedId] = pyEnv["res.partner"].create([
{
name: "Thread followed",
},
{
name: "Thread not followed",
},
]);
pyEnv["mail.followers"].create({
partner_id: currentPartnerId,
res_id: threadFollowedId,
res_model: "res.partner",
});
for (let threadId of [threadFollowedId, threadFollowedId, threadNotFollowedId]) {
for (const threadId of [threadFollowedId, threadFollowedId, threadNotFollowedId]) {
const messageId = pyEnv["mail.message"].create({
body: "not empty",
model: "res.partner",
@@ -861,14 +864,17 @@ QUnit.test('Unfollow message', async function (assert) {
await openDiscuss();
// 2 messages about "Thread followed" with unfollow button and 1 message about "Thread not followed" without it
assert.containsN($, ".o-mail-Message", 3);
for (let messageN of [0, 1]) {
for (const messageN of [0, 1]) {
const $message = $(`.o-mail-Message:eq(${messageN})`);
assert.containsOnce($message, ".o-mail-Message-header:contains(on Thread followed)");
await afterNextRender(() => $message.find("[title='Expand']").click());
assert.containsOnce($message, "[title='Unfollow']");
}
const $messageNotFollowed = $(".o-mail-Message:eq(2)");
assert.containsOnce($messageNotFollowed, ".o-mail-Message-header:contains(on Thread not followed)");
assert.containsOnce(
$messageNotFollowed,
".o-mail-Message-header:contains(on Thread not followed)"
);
await afterNextRender(() => $messageNotFollowed.find("[title='Expand']").click());
assert.containsNone($messageNotFollowed, "[title='Unfollow']");
@@ -876,14 +882,22 @@ QUnit.test('Unfollow message', async function (assert) {
await afterNextRender(() => $message0Followed.find("[title='Expand']").click());
await afterNextRender(() => $message0Followed.find("[title='Unfollow']").click());
assert.containsN($, ".o-mail-Message", 2, "Unfollowing message 0 marks it as read -> Message removed");
assert.containsN(
$,
".o-mail-Message",
2,
"Unfollowing message 0 marks it as read -> Message removed"
);
assert.containsOnce($, ".o-mail-Message-header:contains(on Thread followed)");
assert.containsOnce($, ".o-mail-Message-header:contains(on Thread not followed)");
for (let messageN of [0, 1]) {
for (const messageN of [0, 1]) {
const $message = $(`.o-mail-Message:eq(${messageN})`);
await afterNextRender(() => $message.find("[title='Expand']").click());
assert.containsNone($, "button[title='Unfollow']",
"Unfollowing message 0 -> unfollowing 'Thread followed' -> no more unfollow action on any message");
assert.containsNone(
$,
"button[title='Unfollow']",
"Unfollowing message 0 -> unfollowing 'Thread followed' -> no more unfollow action on any message"
);
}
});
@@ -1301,7 +1315,7 @@ QUnit.test(
);
QUnit.test(
"'Hashtag' thread icon is displayed in top bar of channels of type 'channel' limited to a group",
"Thread avatar image is displayed in top bar of channels of type 'channel' limited to a group",
async (assert) => {
const pyEnv = await startServer();
const groupId = pyEnv["res.groups"].create({ name: "testGroup" });
@@ -1312,12 +1326,12 @@ QUnit.test(
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsOnce($, ".o-mail-Discuss-content .fa-hashtag");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar");
}
);
QUnit.test(
"'Globe' thread icon is displayed in top bar of channels of type 'channel' not limited to any group",
"Thread avatar image is displayed in top bar of channels of type 'channel' not limited to any group",
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({
@@ -1327,7 +1341,7 @@ QUnit.test(
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsOnce($, ".o-mail-Discuss-content .fa-globe");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar");
}
);
@@ -1338,7 +1352,7 @@ QUnit.test(
const [partnerId_1, partnerId_2, partnerId_3, partnerId_4] = pyEnv["res.partner"].create([
{ im_status: "online", name: "Michel Online" },
{ im_status: "offline", name: "Jacqueline Offline" },
{ im_status: "away", name: "Nabuchodonosor Away" },
{ im_status: "away", name: "Nabuchodonosor Idle" },
{ im_status: "im_partner", name: "Robert Fired" },
]);
pyEnv["discuss.channel"].create([
@@ -1381,29 +1395,29 @@ QUnit.test(
const { openDiscuss } = await start();
await openDiscuss();
await click(".o-mail-DiscussCategoryItem:contains('Michel Online')");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Online']");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Online']");
await click(".o-mail-DiscussCategoryItem:contains('Jacqueline Offline')");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Offline']");
await click(".o-mail-DiscussCategoryItem:contains('Nabuchodonosor Away')");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Away']");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Offline']");
await click(".o-mail-DiscussCategoryItem:contains('Nabuchodonosor Idle')");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Idle']");
await click(".o-mail-DiscussCategoryItem:contains('Robert Fired')");
assert.containsOnce(
$,
".o-mail-Discuss-header .o-mail-ThreadIcon [title='No IM status available']"
".o-mail-Discuss-header .o-mail-ImStatus [title='No IM status available']"
);
await click(".o-mail-DiscussCategoryItem:contains('OdooBot')");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Bot']");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Bot']");
}
);
QUnit.test(
"'Users' thread icon is displayed in top bar of channels of type 'group'",
"Thread avatar image is displayed in top bar of channels of type 'group'",
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ channel_type: "group" });
const { openDiscuss } = await start();
await openDiscuss(channelId);
assert.containsOnce($, ".o-mail-Discuss-header .fa-users[title='Grouped Chat']");
assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar");
}
);
@@ -1430,7 +1444,10 @@ QUnit.test(
"Do not trigger channel description server update when channel has no description and editing to empty description",
async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["discuss.channel"].create({ name: "General" });
const channelId = pyEnv["discuss.channel"].create({
create_uid: pyEnv.currentPartnerId,
name: "General",
});
const { openDiscuss } = await start({
mockRPC(route, args, originalRPC) {
if (args.method === "channel_change_description") {
@@ -11,65 +11,61 @@ QUnit.module("im status");
QUnit.test("initially online", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ im_status: "online" });
const channelId = pyEnv["discuss.channel"].create({ name: "TestChanel" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i[title='Online']");
});
QUnit.test("initially offline", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ im_status: "offline" });
const channelId = pyEnv["discuss.channel"].create({ name: "TestChannel" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "offline" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i[title='Offline']");
});
QUnit.test("initially away", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ im_status: "away" });
const channelId = pyEnv["discuss.channel"].create({ name: "TestChanel" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "away" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
const { openDiscuss } = await start();
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i[title='Idle']");
});
QUnit.test("change icon on change partner im_status", async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ im_status: "online" });
const channelId = pyEnv["discuss.channel"].create({ name: "TestChannel" });
pyEnv["mail.message"].create({
author_id: partnerId,
body: "not empty",
model: "discuss.channel",
res_id: channelId,
const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" });
const channelId = pyEnv["discuss.channel"].create({
channel_member_ids: [
Command.create({ partner_id: pyEnv.currentPartnerId }),
Command.create({ partner_id: partnerId }),
],
channel_type: "chat",
});
const { advanceTime, openDiscuss } = await start({ hasTimeControl: true });
await openDiscuss(channelId);
await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY));
assert.containsOnce($, ".o-mail-ImStatus i[title='Online']");
pyEnv["res.partner"].write([partnerId], { im_status: "offline" });
@@ -628,6 +628,22 @@ patch(MockServer.prototype, "mail/models/discuss_channel", {
],
];
}
let is_editable;
switch (channel.channel_type) {
case "channel":
is_editable = channel.create_uid === this.pyEnv.currentPartnerId;
break;
case "chat":
is_editable = true;
break;
case "group":
is_editable = memberOfCurrentUser;
break;
default:
is_editable = false;
break;
}
res.is_editable = is_editable;
res["rtcSessions"] = [
[
"insert",
@@ -1134,36 +1134,6 @@ QUnit.test("chat with author should be opened after clicking on their name", asy
assert.containsOnce($, ".o-mail-ChatWindow-header:contains(Demo User)");
});
QUnit.test(
"chat with author should be opened after clicking on their im status icon",
async (assert) => {
const pyEnv = await startServer();
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
{ name: "Partner_1" },
{ name: "Partner_2", im_status: "online" },
]);
pyEnv["res.users"].create({
im_status: "online",
partner_id: partnerId_2,
});
pyEnv["mail.message"].create({
author_id: partnerId_2,
body: "not empty",
model: "res.partner",
res_id: partnerId_1,
});
const { advanceTime, openFormView } = await start({ hasTimeControl: true });
await openFormView("res.partner", partnerId_1);
await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status
assert.containsOnce($, ".o-mail-ImStatus");
assert.hasClass($(".o-mail-ImStatus"), "cursor-pointer");
await click(".o-mail-ImStatus");
assert.containsOnce($, ".o-mail-ChatWindow");
assert.containsOnce($, ".o-mail-ChatWindow-header:contains(Partner_2)");
}
);
QUnit.test(
"subtype description should be displayed if it is different than body",
async (assert) => {
@@ -51,6 +51,7 @@
<sheet>
<div class="oe_button_box" name="button_box"/>
<field name="avatar_128" invisible="1"/>
<field name="is_editable" invisible="1"/>
<field name="image_128" widget="image" class="oe_avatar" options="{'size': [90, 90], 'preview_image':'avatar_128'}"/>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_title">
@@ -150,6 +150,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': True,
'id': self.channel_general.id,
'invitedMembers': [('insert', [])],
'is_editable': False,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_general.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -199,6 +200,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_channel_public_1.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_channel_public_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -248,6 +250,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_channel_public_2.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_channel_public_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -297,6 +300,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_channel_group_1.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_channel_group_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -346,6 +350,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_channel_group_2.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_channel_group_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -417,6 +422,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_group_1.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_group_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -502,6 +508,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_chat_1.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_chat_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -587,6 +594,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_chat_2.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_chat_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -672,6 +680,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_chat_3.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_chat_3.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -757,6 +766,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_chat_4.id,
'invitedMembers': [('insert', [])],
'is_editable': True,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_chat_4.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -836,6 +846,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_livechat_1.id,
'invitedMembers': [('insert', [])],
'is_editable': False,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_livechat_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -915,6 +926,7 @@ class TestDiscussFullPerformance(TransactionCase):
'group_based_subscription': False,
'id': self.channel_livechat_2.id,
'invitedMembers': [('insert', [])],
'is_editable': False,
'is_minimized': False,
'is_pinned': True,
'last_interest_dt': self.channel_livechat_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
@@ -92,4 +92,8 @@ FileUploader.props = {
fileUploadClass: { type: String, optional: true },
acceptedFileExtensions: { type: String, optional: true },
slots: { type: Object, optional: true },
showUploadingText: { type: Boolean, optional: true },
};
FileUploader.defaultProps = {
showUploadingText: true,
};
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.FileUploader" owl="1">
<t t-if="state.isUploading">Uploading...</t>
<t t-if="state.isUploading and props.showUploadingText">Uploading...</t>
<span t-else="" t-on-click.prevent="onSelectFileButtonClick" style="display:contents">
<t t-slot="toggler"/>
</span>