[FIX] mail: show full company logo in avatar of message

Before this commit, avatar url of author of message always had
style `.o_object_fit_cover`, which makes a nice crop when
ratio of image does not match ratio of the `img`.

When the author of message is a company, it shows the logo of the
company, and it doesn't look nice to crop it.

This commit fixes the issue by using `.o_object_fit_contain` for
avatar of company, so that the logo is fully visible.
When displaying the avatar of a partner whose `is_company` is
undefined, the data is group-fetched, in order for models to
eventually know the value of `is_company` and use the correct
desirable showing.

Task-3381748

closes odoo/odoo#133311

X-original-commit: 48194366f2364e2eb9d99dc4fb71e2de83d8f7a2
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2023-08-29 12:36:01 +00:00
parent b96512a23c
commit 78c2dba93f
15 changed files with 107 additions and 5 deletions
@@ -28,6 +28,7 @@ addModelNamesToFetch([
insertModelFields("res.partner", {
description: { string: "description", type: "text" },
is_company: { default: () => false },
});
//--------------------------------------------------------------------------
+1
View File
@@ -56,6 +56,7 @@ class TestImLivechatMessage(HttpCase):
'attachment_ids': [],
'author': {
'id': self.users[1].partner_id.id,
'is_company': self.users[1].partner_id.is_company,
'name': "test1",
},
'body': message.body,
+1
View File
@@ -878,6 +878,7 @@ class Message(models.Model):
message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids)
author = {
'id': message_sudo.author_id.id,
'is_company': message_sudo.author_id.is_company,
'name': message_sudo.author_id.name,
} if message_sudo.author_id else [('clear',)]
guestAuthor = {
+3 -1
View File
@@ -216,7 +216,7 @@ class Partner(models.Model):
def mail_partner_format(self, fields=None):
partners_format = dict()
if not fields:
fields = {'id': True, 'name': True, 'email': True, 'active': True, 'im_status': True, 'user': {}}
fields = {'id': True, 'name': True, 'email': True, 'active': True, 'im_status': True, 'is_company': True, 'user': {}}
for partner in self:
data = {}
if 'id' in fields:
@@ -229,6 +229,8 @@ class Partner(models.Model):
data['active'] = partner.active
if 'im_status' in fields:
data['im_status'] = partner.im_status
if 'is_company' in fields:
data['is_company'] = partner.is_company
if 'user' in fields:
internal_users = partner.user_ids - partner.user_ids.filtered('share')
main_user = internal_users[0] if len(internal_users) > 0 else partner.user_ids[0] if len(partner.user_ids) > 0 else self.env['res.users']
@@ -164,6 +164,13 @@ export class Message extends Component {
};
}
get authorAvatarAttClass() {
return {
o_object_fit_contain: this.props.message.author?.is_company,
o_object_fit_cover: !this.props.message.author?.is_company,
};
}
get authorAvatarUrl() {
if (
this.message.type === "email" &&
@@ -16,7 +16,7 @@
<div class="o-mail-Message-core position-relative d-flex flex-shrink-0">
<div class="o-mail-Message-sidebar d-flex flex-shrink-0" t-att-class="{ 'justify-content-end': isAlignedRight and !props.squashed, 'align-items-center justify-content-center': props.squashed }">
<div t-if="!props.squashed" class="o-mail-Message-avatarContainer position-relative bg-view">
<img class="o-mail-Message-avatar w-100 h-100 rounded o_object_fit_cover" t-att-src="authorAvatarUrl"/>
<img class="o-mail-Message-avatar w-100 h-100 rounded" t-att-src="authorAvatarUrl" t-att-class="authorAvatarAttClass"/>
</div>
<t t-elif="!message.isTransient">
<small t-if="isActive" class="o-mail-Message-date text-muted opacity-50 ms-2">
@@ -17,6 +17,8 @@ export class Persona extends Record {
localId;
/** @type {number} */
id;
/** @type {boolean | undefined} */
is_company;
/** @type {'partner' | 'guest'} */
type;
/** @type {string} */
@@ -4,6 +4,8 @@ import { Persona } from "@mail/core/common/persona_model";
import { assignDefined, createLocalId, nullifyClearCommands } from "@mail/utils/common/misc";
import { registry } from "@web/core/registry";
import { useSequential } from "@mail/utils/common/hooks";
import { markRaw } from "@odoo/owl";
export const DEFAULT_AVATAR = "/mail/static/src/img/smiley/avatar.jpg";
@@ -19,8 +21,15 @@ export class PersonaService {
setup(env, services) {
this.env = env;
this.rpc = services.rpc;
this.orm = services.orm;
/** @type {import("@mail/core/common/store_service").Store} */
this.store = services["mail.store"];
this.sequential = useSequential();
/** Queue used for handling sequential of fetching is_company of persona */
this._sQueue = markRaw({
/** @type {Set<number>} */
todo: new Set(),
});
}
async updateGuestName(guest, name) {
@@ -61,6 +70,44 @@ export class PersonaService {
}
}
async fetchIsCompany(persona) {
if (persona.type !== "partner") {
// non-partner persona are always considered as not a company
persona.is_company = false;
return;
}
this._sQueue.todo.add(persona.id);
await new Promise(setTimeout); // group synchronous request to fetch is_company
await this.sequential(async () => {
const ongoing = new Set();
if (this._sQueue.todo.size === 0) {
return;
}
// load 'todo' into 'ongoing'
this._sQueue.todo.forEach((id) => ongoing.add(id));
this._sQueue.todo.clear();
// fetch is_company
const partnerData = await this.orm.silent.read(
"res.partner",
[...ongoing],
["is_company"],
{
context: { active_test: false },
}
);
for (const { id, is_company } of partnerData) {
this.insert({ id, is_company, type: "partner" });
ongoing.delete(id);
this._sQueue.todo.delete(id);
}
for (const id of ongoing) {
// no is_company found => assumes persona is not a company
this.insert({ id, is_company: false, type: "partner" });
this._sQueue.todo.delete(id);
}
});
}
/**
* List of known partner ids with a direct chat, ordered
* by most recent interest (1st item being the most recent)
@@ -87,7 +134,7 @@ export class PersonaService {
}
export const personaService = {
dependencies: ["rpc", "mail.store"],
dependencies: ["orm", "rpc", "mail.store"],
/**
* @param {import("@web/env").OdooEnv} env
* @param {Partial<import("services").Services>} services
@@ -50,6 +50,7 @@ export class ThreadService {
this.router = services.router;
this.ui = services.ui;
this.messageService = services["mail.message"];
this.personaService = services["mail.persona"];
}
/**
@@ -1085,6 +1086,9 @@ export class ThreadService {
if (!persona) {
return DEFAULT_AVATAR;
}
if (persona.is_company === undefined) {
this.personaService.fetchIsCompany(persona);
}
if (thread?.model === "discuss.channel") {
if (persona.type === "partner") {
return url(`/discuss/channel/${thread.id}/partner/${persona.id}/avatar_128`);
@@ -1148,7 +1152,16 @@ export class ThreadService {
}
export const threadService = {
dependencies: ["mail.store", "orm", "rpc", "notification", "router", "mail.message", "ui"],
dependencies: [
"mail.store",
"orm",
"rpc",
"notification",
"router",
"mail.message",
"mail.persona",
"ui",
],
/**
* @param {import("@web/env").OdooEnv} env
* @param {Partial<import("services").Services>} services
@@ -23,6 +23,12 @@ patch(Message.prototype, {
this.userService = useService("user");
this.messaging = useState(useService("mail.messaging"));
},
get authorAvatarAttClass() {
return {
...super.authorAvatarAttClass,
"o_redirect cursor-pointer": this.hasAuthorClickable,
};
},
getAuthorText() {
return this.hasAuthorClickable() ? _t("Open profile") : undefined;
},
@@ -33,7 +33,6 @@
<attribute name="t-on-click">ev => this.onClickAuthor(ev)</attribute>
</xpath>
<xpath expr="//span[hasclass('o-mail-Message-author')]" position="attributes">
<attribute name="t-att-class">{ 'cursor-pointer': hasAuthorClickable() }</attribute>
<attribute name="t-att-title">getAuthorText()</attribute>
<attribute name="t-on-click">ev => this.onClickAuthor(ev)</attribute>
</xpath>
@@ -202,6 +202,7 @@ patch(MockServer.prototype, {
});
formattedAuthor = {
id: author.id,
is_company: author.is_company,
name: author.name,
};
} else {
@@ -273,6 +273,7 @@ patch(MockServer.prototype, {
email: partner.email,
id: partner.id,
im_status: partner.im_status,
is_company: partner.is_company,
name: partner.name,
user: mainUser
? {
@@ -146,6 +146,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -196,6 +197,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -246,6 +248,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -296,6 +299,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -346,6 +350,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -397,6 +402,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -417,6 +423,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': False,
'id': self.users[12].partner_id.id,
'im_status': 'offline',
'is_company': False,
'name': 'test12',
'out_of_office_date_end': False,
'user': {
@@ -483,6 +490,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -503,6 +511,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': False,
'id': self.users[14].partner_id.id,
'im_status': 'offline',
'is_company': False,
'name': 'test14',
'out_of_office_date_end': False,
'user': {
@@ -569,6 +578,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -589,6 +599,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': False,
'id': self.users[15].partner_id.id,
'im_status': 'offline',
'is_company': False,
'name': 'test15',
'out_of_office_date_end': False,
'user': {
@@ -655,6 +666,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -675,6 +687,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'test2@example.com',
'id': self.users[2].partner_id.id,
'im_status': 'offline',
'is_company': False,
'name': 'test2',
'out_of_office_date_end': False,
'user': {
@@ -741,6 +754,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -761,6 +775,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': False,
'id': self.users[3].partner_id.id,
'im_status': 'offline',
'is_company': False,
'name': 'test3',
'out_of_office_date_end': False,
'user': {
@@ -998,6 +1013,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'odoobot@example.com',
'id': self.user_root.partner_id.id,
'im_status': 'bot',
'is_company': False,
'name': 'OdooBot',
'out_of_office_date_end': False,
'user': [('clear',)],
@@ -1008,6 +1024,7 @@ class TestDiscussFullPerformance(HttpCase):
'email': 'e.e@example.com',
'id': self.users[0].partner_id.id,
'im_status': 'online',
'is_company': False,
'name': 'Ernest Employee',
'out_of_office_date_end': False,
'user': {
@@ -154,6 +154,10 @@ span.o_force_ltr {
object-fit: cover;
}
.o_object_fit_contain {
object-fit: contain;
}
.o_image_24_cover {
width: 24px;
height: 24px;