[REF] mail: simplify JS discuss models further
- Remove "channel" field in thread formatter.
- Pass `type` for all persona formatters.
- Remove some snake_case to camelCase conversions.
- Add support for inverse fields in discuss JS models.
Details:
1. [REF] mail: remove field 'channel' in thread formatter
This field was used for channel-specific fields, which made
sense when there was a dedicated Channel model that was modeled
with composition with Thread.
To simplify formatter of threads, it's best to flatten props
so that channel-specific fields are immediately available on
thread model. This will improve insertion of data with Threads.
This commit also makes the following other changes:
- remove `discuss.channel/legacy_insert` to use
`mail.record/insert` instead.
- Introduce `toData()` on record, which is helpful to have record
in data format e.g. to pass as a JSON.stringifiable object.
2. [REF] mail: slightly simplify Message.insert from notif
The handling of `mail.record/insert` for Message was handling
transition from starred non-empty message to starred empty message.
To simplify all record insert from server formatted data, the notif
data is just inserted in Message. The adjustment of starred counter
is managed at model level.
This is a prerequisite to significantly simplify all
`mail.record/insert` handling.
3. [REF] mail: rename 'res.users.settings' notifications
Before this commit, notifications related to changes of user
settings were using named notification `mail.record/insert`.
This named notification should be only used for Discuss data that
should be inserted in models. `res.users.settings` is not integrated
in Discuss model, thus it has no reason to use this named
notification.
This commit rename the notification name to `res.users.settings` for
these specific notifications. This prepares simplification on
handling any `mail.record/insert` notifications that should simply
call `Record.insert()`
4. [REF] mail: make dedicate notif for Thread/fold_state
This was using named notif "mail.record/insert", which should
be used to immediately insert data in models. This is however
a dedicated notification to imperatively manager chat window
state based on timing of receiving thread data.
This may eventually become a `mail.record/insert` in the future,
but right now it's much simpler to define it as its own named
notification, in preparation to simplify `mail.record/insert`
notifications handling.
5. [REF] mail: remove Channel in mail.record/insert
This is replaced by `Thread`, so that these data can be
immediately inserted in Thread model.
6. [REF] mail: simplify slightly Attachment.update()
Now that data containing commands is supported, we could
just assign with the command rather than destructure and pick
the dict data part.
7. [REF] mail: introduce assignIn() utils
This function helps reduce LOCs from using the "in" conditional
in sequence:
```js
if (a in data) {
this[a] = data[a];
}
if (b in data) {
this[b] = data[b];
}
if (c in data) {
this[c] = data[c];
}
```
To simply:
```js
assignIn(this, data, [a, b, c]);
```
8. [REF] mail: remove snake_case to camelCase conversion in models
They exist for the sake of keeping Python code snake_case and
JS camelCase. While it's good that each language have a community
that prefer syntax convention, when a codebase uses both languages
and they should work with the same data, it's not great to convert
snake_case to camelCase and vice-versa all the time.
Since server has authority over the data, the server chooses the
format for the keys. Most of them are snake_cased, therefore this
is usually the one we pick.
9. [REF] mail: rename Message.messageReactionGroups to Message.reactions
Easier to read, and matches relation name in JS model
10. [REF] mail: remove explicit assign of some many relations in Message
This reduce amount of custom code in insert(), in preparation to make
all models behave the same in response to inserting data.
11. [REF] mail: rename Thread.customName to Thread.channel_custom_name
To match server data field name, and avoid useless conversion in JS.
12. [REF] mail: simplify Message.insert for recipients
Have formatted data contain `type: "partner"` so it can be assigned
in relational field without adding `type: "partner"` manually in JS.
13. [REF] mail: introduce inverse field in discuss models
With this commit, fields in different models can be linked
together, so that one is mirror of the other field.
This simplifies some `onAdd`/`onDelete` that were added to
sync such fields, and this also simplifies insertion in
relational fields for discuss models that are identified
by records, such as the `MessageReactions` that is identified
by the message and the emoji.
14. [REF] mail: rename CannedResponse.name to 'source'
To make JS model and server data more alike.
15. [REF] mail: remove assignDefined in Persona model
So that eventually all model inserts use `Object.assign()`.
16. [REF] mail: remove 'last_message_id' from channel_info
At some point it was used to display last message in messaging menu.
This is already covered by `channel_fetch_preview` when opening the
messaging menu for the 1st time, so passing `last_message_id` in
channel_info is obsolete.
closes odoo/odoo#137750
Related: odoo/enterprise#48484
Signed-off-by: Matthieu Stockbauer (tsm) <tsm@odoo.com>
This commit is contained in:
@@ -50,7 +50,7 @@ class TestOutOfOffice(TestHrHolidaysCommon):
|
||||
})
|
||||
channel_info = channel._channel_info()[0]
|
||||
# shape of channelMembers is [('ADD', data...)], [0][1] accesses the data
|
||||
members_data = channel_info['channel']['channelMembers'][0][1]
|
||||
members_data = channel_info['channelMembers'][0][1]
|
||||
self.assertEqual(len(members_data), 2, "Channel info should get info for the 2 members")
|
||||
partner_info = next(member for member in members_data if member['persona']['email'] == partner.email)
|
||||
partner2_info = next(member for member in members_data if member['persona']['email'] == partner2.email)
|
||||
|
||||
@@ -51,8 +51,8 @@ class DiscussChannel(models.Model):
|
||||
for channel in self:
|
||||
if channel.chatbot_current_step_id:
|
||||
channel_infos_dict[channel.id]["chatbot_script_id"] = channel.chatbot_current_step_id.chatbot_script_id.id
|
||||
channel_infos_dict[channel.id]['channel']['anonymous_name'] = channel.anonymous_name
|
||||
channel_infos_dict[channel.id]['channel']['anonymous_country'] = {
|
||||
channel_infos_dict[channel.id]['anonymous_name'] = channel.anonymous_name
|
||||
channel_infos_dict[channel.id]['anonymous_country'] = {
|
||||
'code': channel.country_id.code,
|
||||
'id': channel.country_id.id,
|
||||
'name': channel.country_id.name,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { SuggestionService } from "@mail/core/common/suggestion_service";
|
||||
import { cleanTerm } from "@mail/utils/common/format";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(SuggestionService.prototype, {
|
||||
@@ -31,20 +30,14 @@ patch(SuggestionService.prototype, {
|
||||
},
|
||||
|
||||
searchCannedResponseSuggestions(cleanedSearchTerm, sort) {
|
||||
const cannedResponses = Object.values(this.store.CannedResponse.records)
|
||||
.filter((cannedResponse) => {
|
||||
return cleanTerm(cannedResponse.name).includes(cleanedSearchTerm);
|
||||
})
|
||||
.map(({ id, name, substitution }) => {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
substitution: _t(substitution),
|
||||
};
|
||||
});
|
||||
const cannedResponses = Object.values(this.store.CannedResponse.records).filter(
|
||||
(cannedResponse) => {
|
||||
return cleanTerm(cannedResponse.source).includes(cleanedSearchTerm);
|
||||
}
|
||||
);
|
||||
const sortFunc = (c1, c2) => {
|
||||
const cleanedName1 = cleanTerm(c1.name);
|
||||
const cleanedName2 = cleanTerm(c2.name);
|
||||
const cleanedName1 = cleanTerm(c1.source);
|
||||
const cleanedName2 = cleanTerm(c2.source);
|
||||
if (
|
||||
cleanedName1.startsWith(cleanedSearchTerm) &&
|
||||
!cleanedName2.startsWith(cleanedSearchTerm)
|
||||
|
||||
@@ -3,6 +3,8 @@ declare module "models" {
|
||||
livechat: DiscussAppCategory,
|
||||
}
|
||||
export interface Thread {
|
||||
anonymous_country: Object,
|
||||
anonymous_name: String,
|
||||
operator: Persona,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<i class="fa fa-comment-o me-1" aria-label="Lang"/>
|
||||
<t t-esc="member.getLangName()"/>
|
||||
</span>
|
||||
<span t-if="member.persona?.country or props.thread.channel?.anonymous_country">
|
||||
<span t-if="member.persona?.country or props.thread.anonymous_country">
|
||||
<i class="fa fa-globe me-1" aria-label="country"/>
|
||||
<t t-esc="member.persona?.country?.name ?? props.thread.channel.anonymous_country.name"/>
|
||||
<t t-esc="member.persona?.country?.name ?? props.thread.anonymous_country.name"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
import { assignIn } from "@mail/utils/common/misc";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
@@ -10,9 +10,7 @@ patch(Thread, {
|
||||
insert(data) {
|
||||
const thread = super.insert(data);
|
||||
if (thread.type === "livechat") {
|
||||
if (data?.channel) {
|
||||
assignDefined(thread, data.channel, ["anonymous_name"]);
|
||||
}
|
||||
assignIn(thread, data, ["anonymous_name", "anonymous_country"]);
|
||||
if (data?.operator_pid) {
|
||||
thread.operator = {
|
||||
type: "partner",
|
||||
@@ -69,10 +67,8 @@ patch(Thread.prototype, {
|
||||
if (!this.correspondent.is_public && this.correspondent.country) {
|
||||
return `${this.getMemberName(this.correspondent)} (${this.correspondent.country.name})`;
|
||||
}
|
||||
if (this.channel?.anonymous_country) {
|
||||
return `${this.getMemberName(this.correspondent)} (${
|
||||
this.channel.anonymous_country.name
|
||||
})`;
|
||||
if (this.anonymous_country) {
|
||||
return `${this.getMemberName(this.correspondent)} (${this.anonymous_country.name})`;
|
||||
}
|
||||
return this.getMemberName(this.correspondent);
|
||||
},
|
||||
|
||||
@@ -20,11 +20,10 @@ export class LivechatCoreWeb {
|
||||
if (data.current_user_settings?.is_discuss_sidebar_category_livechat_open) {
|
||||
this.store.discuss.livechat.isOpen = true;
|
||||
}
|
||||
this.busService.subscribe("mail.record/insert", (payload) => {
|
||||
const { "res.users.settings": settings } = payload;
|
||||
if (settings) {
|
||||
this.busService.subscribe("res.users.settings", (payload) => {
|
||||
if (payload) {
|
||||
this.store.discuss.livechat.isOpen =
|
||||
settings.is_discuss_sidebar_category_livechat_open ??
|
||||
payload.is_discuss_sidebar_category_livechat_open ??
|
||||
this.store.discuss.livechat.isOpen;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { reactive } from "@odoo/owl";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
@@ -106,6 +107,9 @@ export class LivechatService {
|
||||
* @param {Object} values
|
||||
*/
|
||||
updateSession(values) {
|
||||
if (Record.isRecord(values?.channel)) {
|
||||
values.channel = values.channel.toData();
|
||||
}
|
||||
const session = JSON.parse(cookie.get(this.SESSION_COOKIE) ?? "{}");
|
||||
Object.assign(session, {
|
||||
visitor_uid: this.visitorUid,
|
||||
|
||||
@@ -26,7 +26,7 @@ patch(Thread, {
|
||||
livechatService.updateSession({
|
||||
state: thread.state,
|
||||
seen_message_id: thread.seen_message_id,
|
||||
channel: thread.channel,
|
||||
channel: thread,
|
||||
allow_public_upload: thread.allow_public_upload,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ patch(MockServer.prototype, {
|
||||
const channelInfos = super._mockDiscussChannelChannelInfo(...arguments);
|
||||
for (const channelInfo of channelInfos) {
|
||||
const channel = this.getRecords("discuss.channel", [["id", "=", channelInfo.id]])[0];
|
||||
channelInfo["channel"]["anonymous_name"] = channel.anonymous_name;
|
||||
channelInfo["anonymous_name"] = channel.anonymous_name;
|
||||
// add the last message date
|
||||
if (channel.channel_type === "livechat") {
|
||||
// add the operator id
|
||||
|
||||
@@ -182,11 +182,9 @@ QUnit.test("Open from the bus", async () => {
|
||||
await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel", {
|
||||
count: 0,
|
||||
});
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: settingsId,
|
||||
is_discuss_sidebar_category_livechat_open: true,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: settingsId,
|
||||
is_discuss_sidebar_category_livechat_open: true,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel");
|
||||
});
|
||||
@@ -210,11 +208,9 @@ QUnit.test("Close from the bus", async () => {
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel");
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: settingsId,
|
||||
is_discuss_sidebar_category_livechat_open: false,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: settingsId,
|
||||
is_discuss_sidebar_category_livechat_open: false,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-livechat + .o-mail-DiscussSidebarChannel", {
|
||||
count: 0,
|
||||
|
||||
@@ -36,12 +36,12 @@ class TestGetDiscussChannel(TestImLivechatCommon):
|
||||
'country_id': belgium.id,
|
||||
},
|
||||
)
|
||||
self.assertEqual(channel_info['channel']['anonymous_name'], "Visitor 22")
|
||||
self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'})
|
||||
self.assertEqual(channel_info['anonymous_name'], "Visitor 22")
|
||||
self.assertEqual(channel_info['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'})
|
||||
|
||||
# ensure member info are hidden (in particular email and real name when livechat username is present)
|
||||
# shape of channelMembers is [('ADD', data...)], [0][1] accesses the data
|
||||
self.assertEqual(sorted((m['persona'] for m in channel_info['channel']['channelMembers'][0][1]), key=lambda m: m['id']), sorted([{
|
||||
self.assertEqual(sorted((m['persona'] for m in channel_info['channelMembers'][0][1]), key=lambda m: m['id']), sorted([{
|
||||
'id': self.env['discuss.channel'].browse(channel_info['id']).channel_member_ids.filtered(lambda m: m.guest_id)[0].guest_id.id,
|
||||
'name': 'Visitor',
|
||||
'im_status': 'offline',
|
||||
@@ -64,11 +64,11 @@ class TestGetDiscussChannel(TestImLivechatCommon):
|
||||
'user_id': test_user.id,
|
||||
'channel_id': self.livechat_channel.id,
|
||||
})
|
||||
self.assertFalse(channel_info['channel']['anonymous_name'])
|
||||
self.assertEqual(channel_info['channel']['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'})
|
||||
self.assertEqual(channel_info['channel']['channelMembers'], [['ADD', [
|
||||
self.assertFalse(channel_info['anonymous_name'])
|
||||
self.assertEqual(channel_info['anonymous_country'], {'code': 'BE', 'id': belgium.id, 'name': 'Belgium'})
|
||||
self.assertEqual(channel_info['channelMembers'], [['ADD', [
|
||||
{
|
||||
'channel': {'id': channel_info['id']},
|
||||
'thread': {'id': channel_info['id'], 'model': "discuss.channel"},
|
||||
'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id,
|
||||
'persona': {
|
||||
'active': True,
|
||||
@@ -81,7 +81,7 @@ class TestGetDiscussChannel(TestImLivechatCommon):
|
||||
},
|
||||
},
|
||||
{
|
||||
'channel': {'id': channel_info['id']},
|
||||
'thread': {'id': channel_info['id'], 'model': "discuss.channel"},
|
||||
'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', test_user.partner_id.id)]).id,
|
||||
'persona': {
|
||||
'active': True,
|
||||
@@ -109,11 +109,11 @@ class TestGetDiscussChannel(TestImLivechatCommon):
|
||||
'channel_id': self.livechat_channel.id,
|
||||
})
|
||||
self.assertEqual(channel_info['operator_pid'], [operator.partner_id.id, "Michel Operator"])
|
||||
self.assertFalse(channel_info['channel']['anonymous_name'])
|
||||
self.assertEqual(channel_info['channel']['anonymous_country'], False)
|
||||
self.assertEqual(channel_info['channel']['channelMembers'], [['ADD', [
|
||||
self.assertFalse(channel_info['anonymous_name'])
|
||||
self.assertEqual(channel_info['anonymous_country'], False)
|
||||
self.assertEqual(channel_info['channelMembers'], [['ADD', [
|
||||
{
|
||||
'channel': {'id': channel_info['id']},
|
||||
'thread': {'id': channel_info['id'], 'model': "discuss.channel"},
|
||||
'id': self.env['discuss.channel.member'].search([('channel_id', '=', channel_info['id']), ('partner_id', '=', operator.partner_id.id)]).id,
|
||||
'persona': {
|
||||
'active': True,
|
||||
|
||||
@@ -53,7 +53,7 @@ class TestImLivechatMessage(HttpCase):
|
||||
rating_id=record_rating.id,
|
||||
)
|
||||
self.assertEqual(message.message_format(), [{
|
||||
'attachment_ids': [],
|
||||
'attachments': [],
|
||||
'author': {
|
||||
'id': self.users[1].partner_id.id,
|
||||
'is_company': self.users[1].partner_id.is_company,
|
||||
@@ -75,7 +75,7 @@ class TestImLivechatMessage(HttpCase):
|
||||
'is_note': True,
|
||||
'linkPreviews': [],
|
||||
'message_type': 'notification',
|
||||
'messageReactionGroups': [],
|
||||
'reactions': [],
|
||||
'model': 'discuss.channel',
|
||||
'module_icon': '/mail/static/description/icon.png',
|
||||
'needaction_partner_ids': [],
|
||||
|
||||
@@ -295,9 +295,10 @@ class Channel(models.Model):
|
||||
if 'image_128' in vals:
|
||||
for channel in self:
|
||||
notifications.append([channel, 'mail.record/insert', {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
'avatarCacheKey': channel._get_avatar_cache_key(),
|
||||
'id': channel.id,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
}])
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
@@ -355,10 +356,11 @@ class Channel(models.Model):
|
||||
# post 'channel left' message as root since the partner just unsubscribed from the channel
|
||||
self.sudo().message_post(body=notification, subtype_xmlid="mail.mt_comment", author_id=partner.id)
|
||||
self.env['bus.bus']._sendone(self, 'mail.record/insert', {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
'channelMembers': [('DELETE', {'id': member_id})],
|
||||
'id': self.id,
|
||||
'memberCount': self.member_count,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -431,10 +433,11 @@ class Channel(models.Model):
|
||||
'channel': member.channel_id.sudo()._channel_info()[0],
|
||||
}))
|
||||
notifications.append((channel, 'mail.record/insert', {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
'channelMembers': [('ADD', list(new_members._discuss_channel_member_format().values()))],
|
||||
'id': channel.id,
|
||||
'memberCount': channel.member_count,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
}))
|
||||
if existing_members:
|
||||
@@ -442,10 +445,11 @@ class Channel(models.Model):
|
||||
# In particular this fixes issues where the current user is not aware of its own member in the following case:
|
||||
# create channel from form view, and then join from discuss without refreshing the page.
|
||||
notifications.append((current_partner or current_guest, 'mail.record/insert', {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
'channelMembers': [('ADD', list(existing_members._discuss_channel_member_format().values()))],
|
||||
'id': channel.id,
|
||||
'memberCount': channel.member_count,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
}))
|
||||
if invite_to_rtc_call:
|
||||
@@ -690,7 +694,7 @@ class Channel(models.Model):
|
||||
allowed_company_ids=user_id.company_ids.ids
|
||||
)
|
||||
for channel_info in user_channels._channel_info():
|
||||
notifications.append((partner, 'discuss.channel/legacy_insert', channel_info))
|
||||
notifications.append((partner, 'mail.record/insert', {"Thread": channel_info}))
|
||||
return notifications
|
||||
|
||||
def _channel_message_notifications(self, message, message_format=False):
|
||||
@@ -809,7 +813,6 @@ class Channel(models.Model):
|
||||
return []
|
||||
channel_infos = []
|
||||
rtc_sessions_by_channel = self.sudo().rtc_session_ids._mail_rtc_session_format_by_channel()
|
||||
channel_last_message_ids = dict((r['id'], r['message_id']) for r in self._channel_last_message_ids())
|
||||
current_partner, current_guest = self.env["res.partner"]._get_current_persona()
|
||||
self.env['discuss.channel'].flush_model()
|
||||
self.env['discuss.channel.member'].flush_model()
|
||||
@@ -843,13 +846,10 @@ class Channel(models.Model):
|
||||
if (current_partner and member.partner_id == current_partner) or (current_guest and member.guest_id == current_guest):
|
||||
member_of_current_user_by_channel[member.channel_id] = member
|
||||
for channel in self:
|
||||
channel_data = {
|
||||
info = {
|
||||
'avatarCacheKey': channel._get_avatar_cache_key(),
|
||||
'channel_type': channel.channel_type,
|
||||
'id': channel.id,
|
||||
'memberCount': channel.member_count,
|
||||
}
|
||||
info = {
|
||||
'id': channel.id,
|
||||
'name': channel.name,
|
||||
'defaultDisplayMode': channel.default_display_mode,
|
||||
@@ -862,20 +862,19 @@ class Channel(models.Model):
|
||||
'create_uid': channel.create_uid.id,
|
||||
'authorizedGroupFullName': channel.group_public_id.full_name,
|
||||
'allow_public_upload': channel.allow_public_upload,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
# add last message preview (only used in mobile)
|
||||
info['last_message_id'] = channel_last_message_ids.get(channel.id, False)
|
||||
# find the channel member state
|
||||
if current_partner or current_guest:
|
||||
info['message_needaction_counter'] = channel.message_needaction_counter
|
||||
member = member_of_current_user_by_channel.get(channel, self.env['discuss.channel.member']).with_prefetch([m.id for m in member_of_current_user_by_channel.values()])
|
||||
if member:
|
||||
channel_data['channelMembers'] = [('ADD', list(member._discuss_channel_member_format().values()))]
|
||||
info['channelMembers'] = [('ADD', list(member._discuss_channel_member_format().values()))]
|
||||
info['state'] = member.fold_state or 'open'
|
||||
channel_data['message_unread_counter'] = member.message_unread_counter
|
||||
info['message_unread_counter'] = member.message_unread_counter
|
||||
info['is_minimized'] = member.is_minimized
|
||||
info['seen_message_id'] = member.seen_message_id.id
|
||||
channel_data['custom_channel_name'] = member.custom_channel_name
|
||||
info['custom_channel_name'] = member.custom_channel_name
|
||||
info['is_pinned'] = member.is_pinned
|
||||
info['last_interest_dt'] = member.last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
if member.rtc_inviting_session_id:
|
||||
@@ -885,7 +884,7 @@ class Channel(models.Model):
|
||||
# avoid sending potentially a lot of members for big channels
|
||||
# exclude chat and other small channels from this optimization because they are
|
||||
# assumed to be smaller and it's important to know the member list for them
|
||||
channel_data['channelMembers'] = [('ADD', list(members_by_channel[channel]._discuss_channel_member_format().values()))]
|
||||
info['channelMembers'] = [('ADD', list(members_by_channel[channel]._discuss_channel_member_format().values()))]
|
||||
info['seen_partners_info'] = sorted([{
|
||||
'id': cp.id,
|
||||
'partner_id': cp.partner_id.id,
|
||||
@@ -897,9 +896,6 @@ class Channel(models.Model):
|
||||
'invitedMembers': [('ADD', list(invited_members_by_channel[channel]._discuss_channel_member_format(fields={'id': True, 'channel': {}, 'persona': {'partner': {'id', 'name', 'im_status'}, 'guest': {'id', 'name', 'im_status'}}}).values()))],
|
||||
'rtcSessions': [('ADD', rtc_sessions_by_channel.get(channel, []))],
|
||||
})
|
||||
|
||||
info['channel'] = channel_data
|
||||
|
||||
channel_infos.append(info)
|
||||
return channel_infos
|
||||
|
||||
@@ -925,6 +921,7 @@ class Channel(models.Model):
|
||||
data = {}
|
||||
if 'id' in fields:
|
||||
data['id'] = channel.id
|
||||
data['model'] = "discuss.channel"
|
||||
channels_formatted_data[channel] = data
|
||||
return channels_formatted_data
|
||||
|
||||
@@ -1012,13 +1009,11 @@ class Channel(models.Model):
|
||||
vals['is_minimized'] = is_minimized
|
||||
if vals:
|
||||
session_state.write(vals)
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', {
|
||||
'Thread': {
|
||||
'foldStateCount': state_count,
|
||||
'id': session_state.channel_id.id,
|
||||
'model': 'discuss.channel',
|
||||
'fold_state': state,
|
||||
}
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.Thread/fold_state', {
|
||||
'foldStateCount': state_count,
|
||||
'id': session_state.channel_id.id,
|
||||
'model': 'discuss.channel',
|
||||
'fold_state': state,
|
||||
})
|
||||
|
||||
def channel_pin(self, pinned=False):
|
||||
@@ -1030,7 +1025,7 @@ class Channel(models.Model):
|
||||
if not pinned:
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/unpin', {'id': self.id})
|
||||
else:
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/legacy_insert', self._channel_info()[0])
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', {"Thread": self._channel_info()[0]})
|
||||
|
||||
def _channel_seen(self, last_message_id=None, allow_older=False):
|
||||
"""
|
||||
@@ -1119,9 +1114,10 @@ class Channel(models.Model):
|
||||
member = self.env['discuss.channel.member'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', self.id)])
|
||||
member.write({'custom_channel_name': name})
|
||||
self.env['bus.bus']._sendone(member.partner_id, 'mail.record/insert', {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
'custom_channel_name': name,
|
||||
'id': self.id,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1160,7 +1156,7 @@ class Channel(models.Model):
|
||||
}
|
||||
new_channel.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
|
||||
channel_info = new_channel._channel_info()[0]
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'discuss.channel/legacy_insert', channel_info)
|
||||
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.record/insert', {"Thread": channel_info})
|
||||
return channel_info
|
||||
|
||||
@api.model
|
||||
@@ -1196,10 +1192,8 @@ class Channel(models.Model):
|
||||
channels = self.search(domain, limit=limit)
|
||||
return [{
|
||||
'authorizedGroupFullName': channel.group_public_id.full_name,
|
||||
'channel': {
|
||||
'channel_type': channel.channel_type,
|
||||
'id': channel.id,
|
||||
},
|
||||
'channel_type': channel.channel_type,
|
||||
'model': "discuss.channel",
|
||||
'id': channel.id,
|
||||
'name': channel.name,
|
||||
} for channel in channels]
|
||||
|
||||
@@ -152,7 +152,7 @@ class ChannelMember(models.Model):
|
||||
if 'id' in fields:
|
||||
data['id'] = member.id
|
||||
if 'channel' in fields:
|
||||
data['channel'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id)
|
||||
data['thread'] = member.channel_id._channel_format(fields=fields.get('channel')).get(member.channel_id)
|
||||
if 'persona' in fields:
|
||||
if member.partner_id:
|
||||
persona = member._get_partner_data(fields=fields.get('persona', {}).get('partner'))
|
||||
|
||||
@@ -108,8 +108,7 @@ class ResPartner(models.Model):
|
||||
}
|
||||
partners_format = partners.mail_partner_format()
|
||||
for partner in partners:
|
||||
partners_format.get(partner)["persona"] = {
|
||||
"channelMembers": [
|
||||
partners_format.get(partner)["channelMembers"] = [
|
||||
(
|
||||
"ADD",
|
||||
member_by_partner.get(partner)
|
||||
@@ -122,6 +121,5 @@ class ResPartner(models.Model):
|
||||
)
|
||||
.get(member_by_partner.get(partner)),
|
||||
)
|
||||
],
|
||||
}
|
||||
]
|
||||
return list(partners_format.values())
|
||||
|
||||
@@ -91,7 +91,7 @@ class LinkPreview(models.Model):
|
||||
def _link_preview_format(self):
|
||||
return [{
|
||||
'id': preview.id,
|
||||
'message_id': preview.message_id.id,
|
||||
'message': {'id': preview.message_id.id},
|
||||
'image_mimetype': preview.image_mimetype,
|
||||
'og_description': preview.og_description,
|
||||
'og_image': preview.og_image,
|
||||
|
||||
@@ -870,7 +870,7 @@ class Message(models.Model):
|
||||
"personas": personas,
|
||||
"message": {"id": self.id},
|
||||
}
|
||||
payload = {"Message": {"id": self.id, "messageReactionGroups": [(group_command, group_values)]}}
|
||||
payload = {"Message": {"id": self.id, "reactions": [(group_command, group_values)]}}
|
||||
self.env["bus.bus"]._sendone(self._bus_notification_target(), "mail.record/insert", payload)
|
||||
|
||||
# ------------------------------------------------------
|
||||
@@ -918,10 +918,10 @@ class Message(models.Model):
|
||||
'author': author,
|
||||
'default_subject': default_subject,
|
||||
'notifications': message_sudo.notification_ids._filtered_for_web_client()._notification_format(),
|
||||
'attachment_ids': sorted(message_sudo.attachment_ids._attachment_format(), key=lambda a: a["id"]),
|
||||
'attachments': sorted(message_sudo.attachment_ids._attachment_format(), key=lambda a: a["id"]),
|
||||
'trackingValues': allowed_tracking_ids._tracking_value_format(),
|
||||
'linkPreviews': message_sudo.link_preview_ids._link_preview_format(),
|
||||
'messageReactionGroups': reaction_groups,
|
||||
'reactions': reaction_groups,
|
||||
'pinned_at': message_sudo.pinned_at,
|
||||
'record_name': record_name,
|
||||
'create_date': message_sudo.create_date,
|
||||
@@ -976,7 +976,7 @@ class Message(models.Model):
|
||||
'body': HTML content of the message
|
||||
'model': u'res.partner',
|
||||
'record_name': u'Agrolait',
|
||||
'attachment_ids': [
|
||||
'attachments': [
|
||||
{
|
||||
'file_type_icon': u'webimage',
|
||||
'id': 45,
|
||||
@@ -1042,7 +1042,7 @@ class Message(models.Model):
|
||||
'is_note': message_sudo.subtype_id.id == note_id,
|
||||
'is_discussion': message_sudo.subtype_id.id == com_id,
|
||||
'subtype_description': message_sudo.subtype_id.description,
|
||||
'recipients': [{'id': p.id, 'name': p.name} for p in message_sudo.partner_ids],
|
||||
'recipients': [{'id': p.id, 'name': p.name, 'type': "partner"} for p in message_sudo.partner_ids],
|
||||
'scheduledDatetime': scheduled_dt_by_msg_id.get(vals['id'], False),
|
||||
})
|
||||
if vals['model'] and self.env[vals['model']]._original_module:
|
||||
|
||||
@@ -4139,9 +4139,9 @@ class MailThread(models.AbstractModel):
|
||||
'Message': {
|
||||
'id': message.id,
|
||||
'body': message.body,
|
||||
'attachment_ids': message.attachment_ids.sorted("id")._attachment_format(),
|
||||
'attachments': message.attachment_ids.sorted("id")._attachment_format(),
|
||||
'pinned_at': message.pinned_at,
|
||||
'recipients': [{'id': p.id, 'name': p.name} for p in message.partner_ids],
|
||||
'recipients': [{'id': p.id, 'name': p.name, 'type': "partner"} for p in message.partner_ids],
|
||||
'write_date': message.write_date,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ class ResUsersSettings(models.Model):
|
||||
|
||||
def set_res_users_settings(self, new_settings):
|
||||
formated = super().set_res_users_settings(new_settings)
|
||||
self.env['bus.bus']._sendone(self.user_id.partner_id, 'mail.record/insert', {'res.users.settings': formated})
|
||||
self.env['bus.bus']._sendone(self.user_id.partner_id, 'res.users.settings', formated)
|
||||
return formated
|
||||
|
||||
def set_volume_setting(self, partner_id, volume, guest_id=None):
|
||||
@@ -50,4 +50,4 @@ class ResUsersSettings(models.Model):
|
||||
'partner_id': partner_id,
|
||||
'guest_id': guest_id,
|
||||
})
|
||||
self.env['bus.bus']._sendone(self.user_id.partner_id, 'mail.record/insert', {'res.users.settings.volumes': volume_setting._discuss_users_settings_volume_format()})
|
||||
self.env['bus.bus']._sendone(self.user_id.partner_id, 'res.users.settings.volumes', volume_setting._discuss_users_settings_volume_format())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
import { onChange } from "@mail/utils/common/misc";
|
||||
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
import { FileModelMixin } from "@web/core/file_viewer/file_model";
|
||||
@@ -14,54 +14,30 @@ export class Attachment extends FileModelMixin(Record) {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").Attachment}
|
||||
*/
|
||||
/** @returns {import("models").Attachment} */
|
||||
static insert(data) {
|
||||
if (!("id" in data)) {
|
||||
throw new Error("Cannot insert attachment: id is missing in data");
|
||||
}
|
||||
return super.insert(data);
|
||||
}
|
||||
static new(data) {
|
||||
/** @type {import("models").Attachment} */
|
||||
const attachment = this.preinsert(data);
|
||||
attachment.update(data);
|
||||
const attachment = super.new(data);
|
||||
onChange(attachment, ["extension", "name"], () => {
|
||||
if (!attachment.extension && attachment.name) {
|
||||
attachment.extension = attachment.name.split(".").pop();
|
||||
}
|
||||
});
|
||||
return attachment;
|
||||
}
|
||||
|
||||
update(data) {
|
||||
assignDefined(this, data, [
|
||||
"checksum",
|
||||
"create_date",
|
||||
"filename",
|
||||
"mimetype",
|
||||
"name",
|
||||
"type",
|
||||
"url",
|
||||
"uploading",
|
||||
"extension",
|
||||
"accessToken",
|
||||
"tmpUrl",
|
||||
"message",
|
||||
"res_name",
|
||||
]);
|
||||
if (!("extension" in data) && data["name"]) {
|
||||
this.extension = this.name.split(".").pop();
|
||||
}
|
||||
if (data.originThread !== undefined) {
|
||||
const threadData = Array.isArray(data.originThread)
|
||||
? data.originThread[0][1]
|
||||
: data.originThread;
|
||||
this.originThread = {
|
||||
model: threadData.model,
|
||||
id: threadData.id,
|
||||
};
|
||||
const thread = this.originThread;
|
||||
thread.attachments.add(this);
|
||||
thread.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1));
|
||||
}
|
||||
super.update(data);
|
||||
this.originThread?.attachments.sort((a1, a2) => (a1.id < a2.id ? 1 : -1));
|
||||
}
|
||||
|
||||
originThread = Record.one("Thread");
|
||||
originThread = Record.one("Thread", { inverse: "attachments" });
|
||||
res_name;
|
||||
message = Record.one("Message");
|
||||
/** @type {string} */
|
||||
|
||||
@@ -10,24 +10,15 @@ export class CannedResponse extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").CannedResponse}
|
||||
*/
|
||||
/** @returns {import("models").CannedResponse} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").CannedResponse} */
|
||||
const cannedResponse = this.preinsert(data);
|
||||
Object.assign(cannedResponse, {
|
||||
name: data.source,
|
||||
substitution: data.substitution,
|
||||
});
|
||||
return cannedResponse;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {string} */
|
||||
name;
|
||||
source;
|
||||
/** @type {string} */
|
||||
substitution;
|
||||
}
|
||||
|
||||
@@ -17,35 +17,14 @@ export class ChannelMember extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object|Array} data
|
||||
* @returns {import("models").ChannelMember}
|
||||
*/
|
||||
/** @returns {import("models").ChannelMember} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").ChannelMember} */
|
||||
const member = this.preinsert(data);
|
||||
member.update(data);
|
||||
return member;
|
||||
}
|
||||
|
||||
update(data) {
|
||||
this.id = data.id;
|
||||
if ("persona" in data) {
|
||||
this.persona = data.persona;
|
||||
}
|
||||
let thread = data.thread ?? this.thread;
|
||||
if (!thread && data.channel?.id) {
|
||||
thread = {
|
||||
id: data.channel.id,
|
||||
model: "discuss.channel",
|
||||
};
|
||||
}
|
||||
this.thread ??= thread;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
persona = Record.one("Persona");
|
||||
persona = Record.one("Persona", { inverse: "channelMembers" });
|
||||
rtcSession = Record.one("RtcSession");
|
||||
thread = Record.one("Thread");
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ export class Composer extends Component {
|
||||
options: suggestions.map((suggestion) => {
|
||||
return {
|
||||
cannedResponse: suggestion,
|
||||
name: suggestion.name,
|
||||
source: suggestion.source,
|
||||
label: suggestion.substitution,
|
||||
classList: "o-mail-Composer-suggestion",
|
||||
};
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
<t t-name="mail.Composer.suggestionCannedResponse">
|
||||
<strong class="px-2 py-1 align-self-center flex-shrink-0 text-truncate">
|
||||
<t t-esc="option.name"/>
|
||||
<t t-esc="option.source"/>
|
||||
</strong>
|
||||
<em class="text-600 text-truncate align-self-center">
|
||||
<t t-esc="option.label"/>
|
||||
|
||||
@@ -8,27 +8,13 @@ export class Composer extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").Composer}
|
||||
*/
|
||||
/** @returns {import("models").Composer} */
|
||||
static insert(data) {
|
||||
const { message, thread } = data;
|
||||
if (Boolean(message) === Boolean(thread)) {
|
||||
throw new Error("Composer shall have a thread xor a message.");
|
||||
}
|
||||
/** @type {import("models").Composer} */
|
||||
const composer = this.preinsert(data);
|
||||
if ("textInputContent" in data) {
|
||||
composer.textInputContent = data.textInputContent;
|
||||
}
|
||||
if ("selection" in data) {
|
||||
composer.selection = data.selection;
|
||||
}
|
||||
if ("mentionedPartners" in data) {
|
||||
composer.mentionedPartners = data.mentionedPartners;
|
||||
}
|
||||
return composer;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
attachments = Record.many("Attachment");
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="overflow-auto bg-view d-flex flex-grow-1" t-ref="core">
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<Thread thread="thread" t-key="thread.localId" jumpPresent="state.jumpThreadPresent" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo"/>
|
||||
<Composer t-if="thread.type !== 'mailbox' or thread.eq(messageToReplyTo.thread)" t-key="thread.localId" composer="thread.composer" autofocus="true" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" onDiscardCallback="() => messageToReplyTo.cancel()" onPostCallback.bind="() => this.state.jumpThreadPresent++" dropzoneRef="contentRef" type="messageToReplyTo?.message ? (messageToReplyTo.message.isNote ? 'note' : 'message') : undefined"/>
|
||||
<Composer t-if="thread.type !== 'mailbox' or thread.eq(messageToReplyTo.thread)" t-key="thread.localId" composer="thread.composer" autofocus="true" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" onDiscardCallback="() => messageToReplyTo.cancel()" onPostCallback.bind="() => this.state.jumpThreadPresent++" dropzoneRef="contentRef" type="messageToReplyTo?.message ? (messageToReplyTo.message.is_note ? 'note' : 'message') : undefined"/>
|
||||
</div>
|
||||
<div t-if="threadActions.activeAction?.componentCondition" t-attf-class="{{ threadActions.activeAction.panelOuterClass }}" class="h-100 border-start o-mail-Discuss-inspector">
|
||||
<t t-component="threadActions.activeAction.component" thread="thread" t-props="threadActions.activeAction.componentProps"/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
import { Record } from "./record";
|
||||
|
||||
export class DiscussAppCategory extends Record {
|
||||
@@ -11,10 +10,7 @@ export class DiscussAppCategory extends Record {
|
||||
}
|
||||
/** @returns {import("models").DiscussAppCategory} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").DiscussAppCategory} */
|
||||
const category = this.preinsert(data);
|
||||
assignDefined(category, data);
|
||||
return category;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
|
||||
@@ -12,10 +12,7 @@ export class Follower extends Record {
|
||||
}
|
||||
/** @returns {import("models").Follower} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").Follower} */
|
||||
const follower = this.preinsert(data);
|
||||
Object.assign(follower, data);
|
||||
return follower;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
followedThread = Record.one("Thread");
|
||||
|
||||
@@ -8,24 +8,14 @@ export class LinkPreview extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").LinkPreview}
|
||||
*/
|
||||
/** @returns {import("models").LinkPreview} */
|
||||
static insert(data) {
|
||||
const message = this.store.Message.get(data.message_id);
|
||||
data.message = message;
|
||||
delete data.message_id;
|
||||
/** @type {import("models").LinkPreview} */
|
||||
const linkPreview = this.preinsert(data);
|
||||
Object.assign(linkPreview, data);
|
||||
message?.linkPreviews.add(linkPreview);
|
||||
return linkPreview;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
message = Record.one("Message");
|
||||
message = Record.one("Message", { inverse: "linkPreviews" });
|
||||
/** @type {string} */
|
||||
image_mimetype;
|
||||
/** @type {string} */
|
||||
@@ -58,7 +48,6 @@ export class LinkPreview extends Record {
|
||||
get isCard() {
|
||||
return !this.isImage && !this.isVideo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LinkPreview.register();
|
||||
|
||||
@@ -49,8 +49,8 @@ export class MailCoreCommon {
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
message.delete();
|
||||
this.env.bus.trigger("mail.message/delete", { message });
|
||||
message.delete();
|
||||
}
|
||||
});
|
||||
this.busService.subscribe("mail.message/notification_update", (payload) => {
|
||||
@@ -69,7 +69,20 @@ export class MailCoreCommon {
|
||||
const { message_ids: messageIds, starred } = payload;
|
||||
for (const messageId of messageIds) {
|
||||
const message = this.store.Message.insert({ id: messageId });
|
||||
this.messageService.updateStarred(message, starred);
|
||||
message.isStarred = starred;
|
||||
const starredBox = this.store.discuss.starred;
|
||||
if (starred) {
|
||||
starredBox.counter++;
|
||||
starredBox.messages.add(message);
|
||||
} else {
|
||||
starredBox.counter--;
|
||||
starredBox.messages.delete(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.busService.subscribe("res.users.settings", (payload) => {
|
||||
if (payload) {
|
||||
this.userSettingsService.updateFromCommands(payload);
|
||||
}
|
||||
});
|
||||
this.busService.subscribe("mail.record/insert", (payload) => {
|
||||
@@ -100,18 +113,10 @@ export class MailCoreCommon {
|
||||
}
|
||||
const { Message: messageData } = payload;
|
||||
if (messageData) {
|
||||
const isStarred = this.store.Message.get(messageData.id)?.isStarred;
|
||||
const message = this.store.Message.insert({
|
||||
this.store.Message.insert({
|
||||
...messageData,
|
||||
body: messageData.body ? markup(messageData.body) : messageData.body,
|
||||
});
|
||||
if (isStarred && message.isEmpty) {
|
||||
this.messageService.updateStarred(message, false);
|
||||
}
|
||||
}
|
||||
const { "res.users.settings": settings } = payload;
|
||||
if (settings) {
|
||||
this.userSettingsService.updateFromCommands(settings);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,8 +239,8 @@ export class Message extends Component {
|
||||
|
||||
get showSubtypeDescription() {
|
||||
return (
|
||||
this.message.subtypeDescription &&
|
||||
this.message.subtypeDescription.toLowerCase() !==
|
||||
this.message.subtype_description &&
|
||||
this.message.subtype_description.toLowerCase() !==
|
||||
htmlToTextContentInline(this.message.body || "").toLowerCase()
|
||||
);
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export class Message extends Component {
|
||||
if (this.props.message.type === "auto_comment") {
|
||||
return _t("Automated message");
|
||||
}
|
||||
if (!this.props.message.isDiscussion && this.props.message.type !== "user_notification") {
|
||||
if (!this.props.message.is_discussion && this.props.message.type !== "user_notification") {
|
||||
return _t("Note");
|
||||
}
|
||||
return _t("Message");
|
||||
@@ -262,7 +262,7 @@ export class Message extends Component {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canAddReaction() {
|
||||
return Boolean(!this.message.isTransient && this.message.resId);
|
||||
return Boolean(!this.message.is_transient && this.message.res_id);
|
||||
}
|
||||
|
||||
get deletable() {
|
||||
@@ -284,7 +284,7 @@ export class Message extends Component {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canToggleStar() {
|
||||
return Boolean(!this.message.isTransient && this.message.resId && this.store.user);
|
||||
return Boolean(!this.message.is_transient && this.message.res_id && this.store.user);
|
||||
}
|
||||
|
||||
get showUnfollow() {
|
||||
@@ -440,7 +440,6 @@ export class Message extends Component {
|
||||
const messageContent = convertBrToLineBreak(this.props.message.body);
|
||||
this.props.message.composer = {
|
||||
mentionedPartners: this.props.message.recipients,
|
||||
message: this.props.message,
|
||||
textInputContent: messageContent,
|
||||
selection: {
|
||||
start: messageContent.length,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<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" t-att-src="authorAvatarUrl" t-att-class="authorAvatarAttClass"/>
|
||||
</div>
|
||||
<t t-elif="!message.isTransient">
|
||||
<t t-elif="!message.is_transient">
|
||||
<small t-if="isActive" class="o-mail-Message-date text-muted opacity-50 ms-2">
|
||||
<t t-esc="messageService.dateSimple(message)"/>
|
||||
</small>
|
||||
@@ -36,7 +36,7 @@
|
||||
<strong class="me-1 text-truncate"><t t-if="message.author" t-esc="message.author.name"/><t t-else="" t-esc="message.email_from"/></strong>
|
||||
</span>
|
||||
<t t-if="!isAlignedRight" t-call="mail.Message.notification"/>
|
||||
<small t-if="!message.isTransient" class="o-mail-Message-date text-muted opacity-50" t-att-class="{ 'me-2': !isAlignedRight }" t-att-title="message.datetimeShort">
|
||||
<small t-if="!message.is_transient" class="o-mail-Message-date text-muted opacity-50" t-att-class="{ 'me-2': !isAlignedRight }" t-att-title="message.datetimeShort">
|
||||
<t t-if="shouldDisplayAuthorName">- </t>
|
||||
<RelativeTime datetime="message.datetime"/>
|
||||
</small>
|
||||
@@ -45,11 +45,11 @@
|
||||
className="'ms-1'"
|
||||
message="props.message"
|
||||
thread="props.thread"/>
|
||||
<small t-if="!isOriginThread and !message.isTransient" t-on-click.prevent="openRecord" class="ms-1 text-500">
|
||||
<t t-if="message.resModel !== 'discuss.channel'">
|
||||
<small t-if="!isOriginThread and !message.is_transient" t-on-click.prevent="openRecord" class="ms-1 text-500">
|
||||
<t t-if="message.model !== 'discuss.channel'">
|
||||
on <a t-att-href="message.resUrl"><t t-esc="message.originThread?.displayName"/></a>
|
||||
</t>
|
||||
<t t-if="message.resModel === 'discuss.channel'">
|
||||
<t t-if="message.model === 'discuss.channel'">
|
||||
(from <a t-att-href="message.resUrl"><t t-esc="message.originThread?.prefix"/><t t-esc="message.originThread?.displayName"/></a>)
|
||||
</t>
|
||||
</small>
|
||||
@@ -72,20 +72,20 @@
|
||||
>
|
||||
<div class="o-mail-Message-content o-min-width-0" t-att-class="{ 'w-100': state.isEditing }">
|
||||
<div class="o-mail-Message-textContent position-relative d-flex" t-att-class="{ 'w-100': state.isEditing }">
|
||||
<t t-if="message.type !== 'notification' and !message.isTransient and (message.hasTextContent or message.subtypeDescription or state.isEditing)">
|
||||
<t t-if="message.type !== 'notification' and !message.is_transient and (message.hasTextContent or message.subtype_description or state.isEditing)">
|
||||
<LinkPreviewList t-if="!state.isEditing and message.linkPreviewSquash" linkPreviews="message.linkPreviews" deletable="false"/>
|
||||
<t t-else="">
|
||||
<div class="position-relative overflow-x-auto d-inline-block" t-att-class="{ 'w-100': state.isEditing }">
|
||||
<div class="o-mail-Message-bubble rounded-bottom-3 position-absolute top-0 start-0 w-100 h-100" t-att-class="{
|
||||
'border': state.isEditing and !message.isNote,
|
||||
'bg-info-light border border-info opacity-25': !message.isSelfAuthored and !message.isNote and !message.isHighlightedFromMention,
|
||||
'bg-success-light border border-success opacity-25': message.isSelfAuthored and !message.isNote and !message.isHighlightedFromMention,
|
||||
'border': state.isEditing and !message.is_note,
|
||||
'bg-info-light border border-info opacity-25': !message.isSelfAuthored and !message.is_note and !message.isHighlightedFromMention,
|
||||
'bg-success-light border border-success opacity-25': message.isSelfAuthored and !message.is_note and !message.isHighlightedFromMention,
|
||||
'bg-warning-light border border-warning opacity-50': message.isHighlightedFromMention,
|
||||
}" t-attf-class="{{ isAlignedRight ? 'rounded-start-3' : 'rounded-end-3' }}"/>
|
||||
<div class="position-relative text-break o-mail-Message-body" t-att-class="{
|
||||
'p-1': message.isNote,
|
||||
'mb-0 py-2 px-3': !message.isNote,
|
||||
'align-self-start rounded-end-3 rounded-bottom-3': !state.isEditing and !message.isNote,
|
||||
'p-1': message.is_note,
|
||||
'mb-0 py-2 px-3': !message.is_note,
|
||||
'align-self-start rounded-end-3 rounded-bottom-3': !state.isEditing and !message.is_note,
|
||||
'o-mail-Message-editable flex-grow-1': state.isEditing,
|
||||
}" t-ref="body">
|
||||
<Composer t-if="state.isEditing" autofocus="true" composer="message.composer" messageComponent="constructor" onDiscardCallback.bind="exitEditMode" onPostCallback.bind="exitEditMode" mode="env.inChatter ? 'extended' : 'compact'" sidebar="false"/>
|
||||
@@ -95,7 +95,7 @@
|
||||
<div t-if="message.type === 'email'" t-ref="shadowBody"/>
|
||||
<t t-elif="message.body" t-out="props.messageSearch?.highlight(message.body) ?? message.body"/>
|
||||
<!-- <small t-if="message.editDate" class="o-mail-Message-edited fst-italic text-muted" t-att-class="{ 'ms-2': !message.isBodyEmpty }" t-att-title="message.editDatetimeHuge">(edited)</small> --> <!-- DISABLED because new messages sent by email are wrongly flagged as (edited) -->
|
||||
<t t-if="showSubtypeDescription" t-out="props.messageSearch?.highlight(message.subtypeDescription) ?? message.subtypeDescription"/>
|
||||
<t t-if="showSubtypeDescription" t-out="props.messageSearch?.highlight(message.subtype_description) ?? message.subtype_description"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { htmlToTextContentInline } from "@mail/utils/common/format";
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
import { assignDefined, assignIn, onChange } from "@mail/utils/common/misc";
|
||||
|
||||
import { toRaw } from "@odoo/owl";
|
||||
|
||||
@@ -17,14 +17,23 @@ export class Message extends Record {
|
||||
static id = "id";
|
||||
/** @type {Object.<number, import("models").Message>} */
|
||||
static records = {};
|
||||
static new(data) {
|
||||
const message = super.new(data);
|
||||
onChange(message, "isEmpty", () => {
|
||||
if (message.isEmpty && message.isStarred) {
|
||||
message.isStarred = false;
|
||||
const starred = this.store.discuss.starred;
|
||||
starred.counter--;
|
||||
starred.messages.delete(message);
|
||||
}
|
||||
});
|
||||
return message;
|
||||
}
|
||||
/** @returns {import("models").Message} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").Message}
|
||||
*/
|
||||
/** @returns {import("models").Message} */
|
||||
static insert(data) {
|
||||
if (data.res_id) {
|
||||
this.store.Thread.insert({
|
||||
@@ -32,46 +41,23 @@ export class Message extends Record {
|
||||
id: data.res_id,
|
||||
});
|
||||
}
|
||||
/** @type {import("models").Message} */
|
||||
const message = this.preinsert(data);
|
||||
message.update(data);
|
||||
return message;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @param {Object} data */
|
||||
update(data) {
|
||||
const {
|
||||
attachment_ids: attachments = this.attachments,
|
||||
default_subject: defaultSubject = this.defaultSubject,
|
||||
is_discussion: isDiscussion = this.isDiscussion,
|
||||
is_note: isNote = this.isNote,
|
||||
is_transient: isTransient = this.isTransient,
|
||||
linkPreviews = this.linkPreviews,
|
||||
message_type: type = this.type,
|
||||
model: resModel = this.resModel,
|
||||
module_icon,
|
||||
notifications = this.notifications,
|
||||
parentMessage,
|
||||
recipients = this.recipients,
|
||||
record_name,
|
||||
res_id: resId = this.resId,
|
||||
res_model_name,
|
||||
subtype_description: subtypeDescription = this.subtypeDescription,
|
||||
...remainingData
|
||||
} = data;
|
||||
assignDefined(this, remainingData);
|
||||
assignDefined(this, {
|
||||
defaultSubject,
|
||||
isDiscussion,
|
||||
isNote,
|
||||
isStarred: this._store.user
|
||||
? this.starred_partner_ids.includes(this._store.user.id)
|
||||
: false,
|
||||
isTransient,
|
||||
parentMessage: parentMessage || undefined,
|
||||
resId,
|
||||
resModel,
|
||||
subtypeDescription,
|
||||
type,
|
||||
});
|
||||
// origin thread before other information (in particular notification insert uses it)
|
||||
@@ -85,13 +71,7 @@ export class Message extends Record {
|
||||
: record_name || undefined,
|
||||
});
|
||||
}
|
||||
this.attachments = attachments.map((attachment) => ({ message: this, ...attachment }));
|
||||
if ("author" in data) {
|
||||
this.author = data.author;
|
||||
}
|
||||
this.linkPreviews = linkPreviews.map((data) => ({ ...data, message: this }));
|
||||
this.notifications = notifications.map((notif) => ({ ...notif, message: this }));
|
||||
this.recipients = recipients.map((recipient) => ({ ...recipient, type: "partner" }));
|
||||
assignIn(this, data, ["author", "notifications", "reactions", "recipients"]);
|
||||
if ("user_follower_id" in data && data.user_follower_id && this._store.self) {
|
||||
this.originThread.selfFollower = {
|
||||
followedThread: this.originThread,
|
||||
@@ -100,9 +80,6 @@ export class Message extends Record {
|
||||
partner: this._store.self,
|
||||
};
|
||||
}
|
||||
if ("messageReactionGroups" in data) {
|
||||
this.reactions = data.messageReactionGroups;
|
||||
}
|
||||
if (this.isNotification && !this.notificationType) {
|
||||
const parser = new DOMParser();
|
||||
const htmlBody = parser.parseFromString(this.body, "text/html");
|
||||
@@ -110,36 +87,36 @@ export class Message extends Record {
|
||||
}
|
||||
}
|
||||
|
||||
attachments = Record.many("Attachment");
|
||||
attachments = Record.many("Attachment", { inverse: "message" });
|
||||
author = Record.one("Persona");
|
||||
/** @type {string} */
|
||||
body;
|
||||
composer = Record.one("Composer", { onDelete: (r) => r.delete() });
|
||||
composer = Record.one("Composer", { inverse: "message", onDelete: (r) => r.delete() });
|
||||
/** @type {string} */
|
||||
defaultSubject;
|
||||
default_subject;
|
||||
/** @type {number|string} */
|
||||
id;
|
||||
/** @type {boolean} */
|
||||
isDiscussion;
|
||||
is_discussion;
|
||||
/** @type {boolean} */
|
||||
isNote;
|
||||
is_note;
|
||||
/** @type {boolean} */
|
||||
isStarred;
|
||||
/** @type {boolean} */
|
||||
isTransient;
|
||||
linkPreviews = Record.many("LinkPreview");
|
||||
is_transient;
|
||||
linkPreviews = Record.many("LinkPreview", { inverse: "message" });
|
||||
/** @type {number[]} */
|
||||
needaction_partner_ids = [];
|
||||
/** @type {number[]} */
|
||||
history_partner_ids = [];
|
||||
parentMessage = Record.one("Message");
|
||||
reactions = Record.many("MessageReactions");
|
||||
notifications = Record.many("Notification");
|
||||
reactions = Record.many("MessageReactions", { inverse: "message" });
|
||||
notifications = Record.many("Notification", { inverse: "message" });
|
||||
recipients = Record.many("Persona");
|
||||
/** @type {number|string} */
|
||||
resId;
|
||||
res_id;
|
||||
/** @type {string|undefined} */
|
||||
resModel;
|
||||
model;
|
||||
/** @type {string} */
|
||||
scheduledDatetime;
|
||||
/** @type {Number[]} */
|
||||
@@ -147,7 +124,7 @@ export class Message extends Record {
|
||||
/** @type {string} */
|
||||
subject;
|
||||
/** @type {string} */
|
||||
subtypeDescription;
|
||||
subtype_description;
|
||||
/** @type {Object[]} */
|
||||
trackingValues = [];
|
||||
/** @type {string} */
|
||||
@@ -207,7 +184,7 @@ export class Message extends Record {
|
||||
}
|
||||
|
||||
get isHighlightedFromMention() {
|
||||
return this.isSelfMentioned && this.resModel === "discuss.channel";
|
||||
return this.isSelfMentioned && this.model === "discuss.channel";
|
||||
}
|
||||
|
||||
get isSelfAuthored() {
|
||||
@@ -222,7 +199,7 @@ export class Message extends Record {
|
||||
}
|
||||
|
||||
get hasActions() {
|
||||
return !this.isTransient;
|
||||
return !this.is_transient;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +213,7 @@ export class Message extends Record {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isNotification() {
|
||||
return this.type === "notification" && this.resModel === "discuss.channel";
|
||||
return this.type === "notification" && this.model === "discuss.channel";
|
||||
}
|
||||
|
||||
get isSubjectSimilarToOriginThreadName() {
|
||||
@@ -251,17 +228,17 @@ export class Message extends Record {
|
||||
|
||||
get isSubjectDefault() {
|
||||
const threadName = this.originThread?.name?.trim().toLowerCase();
|
||||
const defaultSubject = this.defaultSubject ? this.defaultSubject.toLowerCase() : "";
|
||||
const defaultSubject = this.default_subject ? this.default_subject.toLowerCase() : "";
|
||||
const candidates = new Set([defaultSubject, threadName]);
|
||||
return candidates.has(this.subject?.toLowerCase());
|
||||
}
|
||||
|
||||
get originThread() {
|
||||
return this._store.Thread.get({ model: this.resModel, id: this.resId });
|
||||
return this._store.Thread.get({ model: this.model, id: this.res_id });
|
||||
}
|
||||
|
||||
get resUrl() {
|
||||
return `${url("/web")}#model=${this.resModel}&id=${this.resId}`;
|
||||
return `${url("/web")}#model=${this.model}&id=${this.res_id}`;
|
||||
}
|
||||
|
||||
get editDate() {
|
||||
@@ -277,7 +254,7 @@ export class Message extends Record {
|
||||
this.isBodyEmpty &&
|
||||
this.attachments.length === 0 &&
|
||||
this.trackingValues.length === 0 &&
|
||||
!this.subtypeDescription
|
||||
!this.subtype_description
|
||||
);
|
||||
}
|
||||
get isBodyEmpty() {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<div class="position-relative d-flex flex-wrap"
|
||||
t-att-class="{
|
||||
'flex-row-reverse me-3': env.inChatWindow and env.alignedRight,
|
||||
'ms-3': !(env.inChatWindow and env.alignedRight) and (props.message.isDiscussion),
|
||||
'ms-3': !(env.inChatWindow and env.alignedRight) and (props.message.is_discussion),
|
||||
}"
|
||||
t-attf-class="{{ props.message.isDiscussion ? 'mt-n2' : 'mt-1' }}">
|
||||
t-attf-class="{{ props.message.is_discussion ? 'mt-n2' : 'mt-1' }}">
|
||||
<button t-foreach="props.message.reactions" t-as="reaction" t-key="reaction.content" class="o-mail-MessageReaction btn d-flex p-0 border rounded-1 mb-1"
|
||||
t-on-click="() => this.onClickReaction(reaction)"
|
||||
t-on-contextmenu="onContextMenu"
|
||||
|
||||
@@ -8,21 +8,9 @@ export class MessageReactions extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").MessageReactions}
|
||||
*/
|
||||
/** @returns {import("models").MessageReactions} */
|
||||
static insert(data) {
|
||||
if (data.message && !(data.message instanceof Record)) {
|
||||
data.message = this.store.Message.insert(data.message);
|
||||
}
|
||||
let reaction = data.message.reactions.find(({ content }) => content === data.content);
|
||||
if (!reaction) {
|
||||
/** @type {import("models").MessageReactions} */
|
||||
reaction = this.preinsert(data);
|
||||
}
|
||||
Object.assign(reaction, data);
|
||||
return reaction;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
|
||||
@@ -191,18 +191,6 @@ export class MessageService {
|
||||
);
|
||||
}
|
||||
|
||||
updateStarred(message, isStarred) {
|
||||
message.isStarred = isStarred;
|
||||
const starred = this.store.discuss.starred;
|
||||
if (isStarred) {
|
||||
starred.counter++;
|
||||
starred.messages.add(message);
|
||||
} else {
|
||||
starred.counter--;
|
||||
removeFromArrayWithPredicate(starred.messages, (msg) => msg.eq(message));
|
||||
}
|
||||
}
|
||||
|
||||
scheduledDateSimple(message) {
|
||||
return message.scheduledDate.toLocaleString(DateTime.TIME_SIMPLE, {
|
||||
locale: this.userService.lang?.replace("_", "-"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { assignIn } from "@mail/utils/common/misc";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
@@ -45,9 +46,7 @@ export class NotificationGroup extends Record {
|
||||
type: data.type ?? this.type,
|
||||
status: data.status ?? this.status,
|
||||
});
|
||||
if ("notifications" in data) {
|
||||
this.notifications = data.notifications;
|
||||
}
|
||||
assignIn(this, data, ["notifications"]);
|
||||
this.lastMessage = this.notifications[0]?.message;
|
||||
for (const notification of this.notifications) {
|
||||
if (this.lastMessage?.id < notification.message?.id) {
|
||||
|
||||
@@ -12,15 +12,9 @@ export class Notification extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").Notification}
|
||||
*/
|
||||
/** @returns {import("models").Notification} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").Notification} */
|
||||
const notification = this.preinsert(data);
|
||||
notification.update(data);
|
||||
return notification;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
update(data) {
|
||||
@@ -37,7 +31,7 @@ export class Notification extends Record {
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
if (!this.message.author?.eq(this._store.self)) {
|
||||
if (!this.message?.author?.eq(this._store.self)) {
|
||||
return;
|
||||
}
|
||||
const thread = this.message.originThread;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class OutOfFocusService {
|
||||
async notify(message, channel) {
|
||||
const modelsHandleByPush = ["mail.thread", "discuss.channel"];
|
||||
if (
|
||||
modelsHandleByPush.includes(message.resModel) &&
|
||||
modelsHandleByPush.includes(message.model) &&
|
||||
(await this.hasServiceWorkInstalledAndPushSubscriptionActive())
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { AND, Record } from "@mail/core/common/record";
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
|
||||
/**
|
||||
* @typedef {'offline' | 'bot' | 'online' | 'away' | 'im_partner' | undefined} ImStatus
|
||||
@@ -21,16 +20,13 @@ export class Persona extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Data} data
|
||||
* @returns {import("models").Persona}
|
||||
*/
|
||||
/** @returns {import("models").Persona} */
|
||||
static insert(data) {
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
update(data) {
|
||||
assignDefined(this, data);
|
||||
super.update(data);
|
||||
if (
|
||||
this.type === "partner" &&
|
||||
this.im_status !== "im_partner" &&
|
||||
@@ -41,6 +37,7 @@ export class Persona extends Record {
|
||||
}
|
||||
}
|
||||
|
||||
channelMembers = Record.many("ChannelMember");
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {boolean | undefined} */
|
||||
|
||||
@@ -9,6 +9,7 @@ const MANY_SYM = Symbol("many");
|
||||
const ONE_SYM = Symbol("one");
|
||||
const OR_SYM = Symbol("or");
|
||||
const AND_SYM = Symbol("and");
|
||||
const IS_RECORD_SYM = Symbol("isRecord");
|
||||
|
||||
export function AND(...args) {
|
||||
return [AND_SYM, ...args];
|
||||
@@ -27,14 +28,14 @@ export function OR(...args) {
|
||||
export function preinsert(val, record, fname, fn) {
|
||||
/** @type {R} */
|
||||
let r3;
|
||||
if (!(val instanceof Record)) {
|
||||
if (!Record.isRecord(val)) {
|
||||
const { targetModel } = record.Model.__rels__.get(fname);
|
||||
r3 = record.Model.store[targetModel].preinsert(val);
|
||||
} else {
|
||||
r3 = val;
|
||||
}
|
||||
fn(r3);
|
||||
if (!(val instanceof Record)) {
|
||||
if (!Record.isRecord(val)) {
|
||||
// was preinserted, fully insert now
|
||||
const { targetModel } = record.Model.__rels__.get(fname);
|
||||
record.Model.store[targetModel].insert(val);
|
||||
@@ -89,6 +90,12 @@ export class RecordInverses {
|
||||
* @template {Record} R
|
||||
*/
|
||||
export class RecordList extends Array {
|
||||
static isOne(list) {
|
||||
return Boolean(list[ONE_SYM]);
|
||||
}
|
||||
static isMany(list) {
|
||||
return Boolean(list[MANY_SYM]);
|
||||
}
|
||||
/** @type {Record} */
|
||||
owner;
|
||||
/** @type {string} */
|
||||
@@ -107,8 +114,10 @@ export class RecordList extends Array {
|
||||
/** @type {string[]} */
|
||||
__list__ = [];
|
||||
|
||||
constructor() {
|
||||
/** @param {ONE_SYM|MANY_SYM} SYM */
|
||||
constructor(SYM) {
|
||||
super();
|
||||
this[SYM] = true;
|
||||
return new Proxy(this, {
|
||||
/** @param {RecordList<R>} receiver */
|
||||
get(target, name, receiver) {
|
||||
@@ -132,13 +141,23 @@ export class RecordList extends Array {
|
||||
if (r2 && r2.notEq(r3)) {
|
||||
receiver.__deleteInverse__(r2);
|
||||
}
|
||||
const { onDelete } = receiver.owner.Model.__rels__.get(receiver.name);
|
||||
const { inverse, onDelete } = receiver.owner.Model.__rels__.get(
|
||||
receiver.name
|
||||
);
|
||||
onDelete?.call(receiver.owner, r2);
|
||||
if (inverse) {
|
||||
r2.__rels__.get(inverse).delete(receiver);
|
||||
}
|
||||
receiver.__list__[index] = r3?.localId;
|
||||
if (r3) {
|
||||
receiver.__addInverse__(r3);
|
||||
const { onAdd } = receiver.owner.Model.__rels__.get(receiver.name);
|
||||
const { inverse, onAdd } = receiver.owner.Model.__rels__.get(
|
||||
receiver.name
|
||||
);
|
||||
onAdd?.call(receiver.owner, r3);
|
||||
if (inverse) {
|
||||
r3.__rels__.get(inverse).add(receiver);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (name === "length") {
|
||||
@@ -159,6 +178,11 @@ export class RecordList extends Array {
|
||||
* @param {(R) => void} fn
|
||||
*/
|
||||
_preinsert(val, fn) {
|
||||
const { inverse } = this.owner.Model.__rels__.get(this.name);
|
||||
if (inverse) {
|
||||
// special command to call __addNoinv, to prevent infinite loop
|
||||
val[inverse] = [["ADD.noinv", this.owner]];
|
||||
}
|
||||
return preinsert(val, this.owner, this.name, fn);
|
||||
}
|
||||
/**
|
||||
@@ -175,8 +199,11 @@ export class RecordList extends Array {
|
||||
this.__list__.push(r3.localId);
|
||||
this.__addInverse__(r3);
|
||||
});
|
||||
const { onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
onAdd?.call(this.owner, r);
|
||||
if (inverse) {
|
||||
r.__rels__.get(inverse).add(this.owner);
|
||||
}
|
||||
}
|
||||
return this.__list__.length;
|
||||
}
|
||||
@@ -186,8 +213,11 @@ export class RecordList extends Array {
|
||||
if (r2) {
|
||||
this.__deleteInverse__(r2);
|
||||
}
|
||||
const { onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
onDelete?.call(this.owner, r2);
|
||||
if (inverse) {
|
||||
r2.__rels__.get(inverse).delete(this.owner);
|
||||
}
|
||||
return r2;
|
||||
}
|
||||
/** @returns {R} */
|
||||
@@ -196,8 +226,11 @@ export class RecordList extends Array {
|
||||
if (r2) {
|
||||
this.__deleteInverse__(r2);
|
||||
}
|
||||
const { onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
onDelete?.call(this.owner, r2);
|
||||
if (inverse) {
|
||||
r2.__rels__.get(inverse).delete(this.owner);
|
||||
}
|
||||
return r2;
|
||||
}
|
||||
/** @param {R[]} records */
|
||||
@@ -207,8 +240,11 @@ export class RecordList extends Array {
|
||||
this.__list__.unshift(r3.localId);
|
||||
this.__addInverse__(r3);
|
||||
});
|
||||
const { onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
onAdd?.call(this.owner, r);
|
||||
if (inverse) {
|
||||
r.__rels__.get(inverse).add(this.owner);
|
||||
}
|
||||
}
|
||||
return this.__list__.length;
|
||||
}
|
||||
@@ -279,13 +315,19 @@ export class RecordList extends Array {
|
||||
this.__list__.splice(start, deleteCount, ...newRecords.map((r) => r.localId));
|
||||
for (const r of oldRecords) {
|
||||
this.__deleteInverse__(r);
|
||||
const { onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onDelete } = this.owner.Model.__rels__.get(this.name);
|
||||
onDelete?.call(this.owner, r);
|
||||
if (inverse) {
|
||||
r.__rels__.get(inverse).delete(this.owner);
|
||||
}
|
||||
}
|
||||
for (const r of newRecords) {
|
||||
this.__addInverse__(r);
|
||||
const { onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
const { inverse, onAdd } = this.owner.Model.__rels__.get(this.name);
|
||||
onAdd?.call(this.owner, r);
|
||||
if (inverse) {
|
||||
r.__rels__.get(inverse).add(this.owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @param {(a: R, b: R) => boolean} func */
|
||||
@@ -300,7 +342,22 @@ export class RecordList extends Array {
|
||||
}
|
||||
/** @param {...R} */
|
||||
add(...records) {
|
||||
if (RecordList.isOne(this)) {
|
||||
const last = records.at(-1);
|
||||
if (Record.isRecord(last) && last.in(this)) {
|
||||
return;
|
||||
}
|
||||
this._preinsert(last, (r) => {
|
||||
if (r.notEq(this[0])) {
|
||||
this.splice(0, 1, r);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const val of records) {
|
||||
if (Record.isRecord(val) && val.in(this)) {
|
||||
continue;
|
||||
}
|
||||
this._preinsert(val, (r) => {
|
||||
if (this.indexOf(r) === -1) {
|
||||
this.push(r);
|
||||
@@ -308,6 +365,40 @@ export class RecordList extends Array {
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Version of add() that does not update the inverse.
|
||||
* This is internally call when assigning on relational field with inverse,
|
||||
* to prevent infinite loops.
|
||||
*
|
||||
* @param {...R}
|
||||
*/
|
||||
__addNoinv(...records) {
|
||||
if (RecordList.isOne(this)) {
|
||||
const last = records.at(-1);
|
||||
if (Record.isRecord(last) && last.in(this)) {
|
||||
return;
|
||||
}
|
||||
this._preinsert(last, (r) => {
|
||||
if (r.notEq(this[0])) {
|
||||
this.splice(0, 1);
|
||||
this.__list__.push(r.localId);
|
||||
this.__addInverse__(r);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const val of records) {
|
||||
if (Record.isRecord(val) && val.in(this)) {
|
||||
continue;
|
||||
}
|
||||
this._preinsert(val, (r) => {
|
||||
if (this.indexOf(r) === -1) {
|
||||
this.__list__.push(r.localId);
|
||||
this.__addInverse__(r);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/** @param {...R} */
|
||||
delete(...records) {
|
||||
for (const val of records) {
|
||||
@@ -344,6 +435,9 @@ export class Record {
|
||||
* @type {Map<string, any>}
|
||||
*/
|
||||
static __rels__ = new Map();
|
||||
static isRecord(record) {
|
||||
return Boolean(record?.[IS_RECORD_SYM]);
|
||||
}
|
||||
static get(data) {
|
||||
return this.records[this.localId(data)];
|
||||
}
|
||||
@@ -365,6 +459,18 @@ export class Record {
|
||||
static _localId(expr, data, { brackets = false } = {}) {
|
||||
if (!Array.isArray(expr)) {
|
||||
if (this.__rels__.has(expr)) {
|
||||
if (RecordList.isMany(this.__rels__.get(expr))) {
|
||||
throw new Error("Using a Record.Many() as id is not (yet) supported");
|
||||
}
|
||||
if (this.isCommand(data[expr])) {
|
||||
// Note: only Record.one() is supported
|
||||
const [cmd, data2] = data[expr].at(-1);
|
||||
if (cmd === "DELETE") {
|
||||
return undefined;
|
||||
} else {
|
||||
return `(${data2?.localId})`;
|
||||
}
|
||||
}
|
||||
// relational field (note: optional when OR)
|
||||
return `(${data[expr]?.localId})`;
|
||||
}
|
||||
@@ -384,6 +490,17 @@ export class Record {
|
||||
const res = {};
|
||||
function _deepRetrieve(expr2) {
|
||||
if (typeof expr2 === "string") {
|
||||
if (Record.isCommand(data[expr2])) {
|
||||
// Note: only Record.one() is supported
|
||||
const [cmd, data2] = data[expr2].at(-1);
|
||||
if (cmd === "DELETE") {
|
||||
return Object.assign(res, { [expr2]: undefined });
|
||||
} else if (cmd === "ADD.noinv") {
|
||||
return Object.assign(res, { [expr2]: [["ADD.noinv", data2]] });
|
||||
} else {
|
||||
return Object.assign(res, { [expr2]: data2 });
|
||||
}
|
||||
}
|
||||
return Object.assign(res, { [expr2]: data[expr2] });
|
||||
}
|
||||
if (expr2 instanceof Array) {
|
||||
@@ -399,6 +516,15 @@ export class Record {
|
||||
return res;
|
||||
}
|
||||
if (typeof this.id === "string") {
|
||||
if (Record.isCommand(data[this.id])) {
|
||||
// Note: only Record.one() is supported
|
||||
const [cmd, data2] = data[this.id].at(-1);
|
||||
if (cmd === "DELETE") {
|
||||
return { [this.id]: undefined };
|
||||
} else {
|
||||
return { [this.id]: data2 };
|
||||
}
|
||||
}
|
||||
return { [this.id]: data[this.id] };
|
||||
}
|
||||
for (const expr of this.id) {
|
||||
@@ -428,7 +554,12 @@ export class Record {
|
||||
static new(data) {
|
||||
const obj = new this.Class();
|
||||
const ids = this._retrieveIdFromData(data);
|
||||
let record = Object.assign(obj, { Model: this, localId: this.localId(data), ...ids });
|
||||
let record = Object.assign(obj, {
|
||||
[IS_RECORD_SYM]: true,
|
||||
Model: this,
|
||||
localId: this.localId(data),
|
||||
...ids,
|
||||
});
|
||||
Object.assign(record, { _store: this.store });
|
||||
this.records[record.localId] = record;
|
||||
// return reactive version
|
||||
@@ -438,31 +569,30 @@ export class Record {
|
||||
/**
|
||||
* @template {keyof import("model ").Models} M
|
||||
* @param {M} targetModel
|
||||
* @param {(r) => void} [onAdd] function that is called when a record is added
|
||||
* @param {string} [inverse] if set, the name of field in targetModel that acts as the inverse.
|
||||
* @param {(r: M) => void} [onAdd] function that is called when a record is added
|
||||
* in the relation.
|
||||
* @param {(r) => void} [onDelete] function that is called when a record is removed
|
||||
* @param {(r: M) => void} [onDelete] function that is called when a record is removed
|
||||
* from the relation.
|
||||
* @returns {import("models").Models[M]}
|
||||
*/
|
||||
static one(targetModel, { onAdd, onDelete } = {}) {
|
||||
return [ONE_SYM, { targetModel, onAdd, onDelete }];
|
||||
static one(targetModel, { inverse, onAdd, onDelete } = {}) {
|
||||
return [ONE_SYM, { targetModel, inverse, onAdd, onDelete }];
|
||||
}
|
||||
/**
|
||||
* @template {keyof import("model").Models} M
|
||||
* @param {M} targetModel
|
||||
* @param {(r) => void} [onAdd] function that is called when a record is added
|
||||
* @param {string} [inverse] if set, the name of field in targetModel that acts as the inverse.
|
||||
* @param {(r: M) => void} [onAdd] function that is called when a record is added
|
||||
* in the relation.
|
||||
* @param {(r) => void} [onDelete] function that is called when a record is removed
|
||||
* @param {(r: M) => void} [onDelete] function that is called when a record is removed
|
||||
* from the relation.
|
||||
* @returns {import("models").Models[M][]}
|
||||
*/
|
||||
static many(targetModel, { onAdd, onDelete } = {}) {
|
||||
return [MANY_SYM, { targetModel, onAdd, onDelete }];
|
||||
static many(targetModel, { inverse, onAdd, onDelete } = {}) {
|
||||
return [MANY_SYM, { targetModel, inverse, onAdd, onDelete }];
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {Record}
|
||||
*/
|
||||
/** @returns {Record} */
|
||||
static insert(data) {
|
||||
const res = this.preinsert(data);
|
||||
res.update(data);
|
||||
@@ -476,7 +606,7 @@ export class Record {
|
||||
return this.get(data) ?? this.new(data);
|
||||
}
|
||||
static isCommand(data) {
|
||||
return ["ADD", "DELETE"].includes(data?.[0]?.[0]);
|
||||
return ["ADD", "DELETE", "ADD.noinv"].includes(data?.[0]?.[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,13 +614,14 @@ export class Record {
|
||||
* rather than the record(s). This allows data in store and models being normalized,
|
||||
* which eases handling relations notably in when a record gets deleted.
|
||||
*
|
||||
* @type {Map<string, string|RecordList>}
|
||||
* @type {Map<string, RecordList>}
|
||||
*/
|
||||
__rels__ = new Map();
|
||||
/** @type {Map<string, { targetModel: string }>} */
|
||||
__invs__ = new RecordInverses();
|
||||
/** @type {import("@mail/core/common/store_service").Store} */
|
||||
_store;
|
||||
get _store() {
|
||||
return this.Model.store;
|
||||
}
|
||||
/**
|
||||
* Technical attribute, contains the Model entry in the store.
|
||||
* This is almost the same as the class, except it's an object
|
||||
@@ -515,7 +646,9 @@ export class Record {
|
||||
|
||||
setup() {}
|
||||
|
||||
update(data) {}
|
||||
update(data) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
delete() {
|
||||
const r1 = this;
|
||||
@@ -531,7 +664,7 @@ export class Record {
|
||||
continue;
|
||||
}
|
||||
const l2 = r2.__rels__.get(name2);
|
||||
if (l2 instanceof RecordList) {
|
||||
if (RecordList.isMany(l2)) {
|
||||
for (let c = 0; c < count; c++) {
|
||||
r2[name2].delete(r1);
|
||||
}
|
||||
@@ -540,10 +673,7 @@ export class Record {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.Model) {
|
||||
delete this.Model.records[r1.localId];
|
||||
delete this.Model;
|
||||
}
|
||||
delete this.Model.records[r1.localId];
|
||||
}
|
||||
|
||||
/** @param {Record} record */
|
||||
@@ -572,6 +702,31 @@ export class Record {
|
||||
notIn(collection) {
|
||||
return !this.in(collection);
|
||||
}
|
||||
|
||||
toData() {
|
||||
const data = { ...this };
|
||||
for (const [name, val] of this.__rels__.entries()) {
|
||||
if (RecordList.isMany(val)) {
|
||||
data[name] = val.map((r) => r.toIdData());
|
||||
} else {
|
||||
data[name] = this[name]?.toIdData();
|
||||
}
|
||||
}
|
||||
delete data._store;
|
||||
delete data.__rels__;
|
||||
delete data.__invs__;
|
||||
delete data.Model;
|
||||
return data;
|
||||
}
|
||||
toIdData() {
|
||||
const data = this.constructor._retrieveIdFromData(this);
|
||||
for (const [name, val] of Object.entries(data)) {
|
||||
if (Record.isRecord(val)) {
|
||||
data[name] = val.toIdData();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
Record.register();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { reactive } from "@odoo/owl";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { modelRegistry, preinsert, Record, RecordInverses, RecordList } from "./record";
|
||||
import { modelRegistry, Record, RecordInverses, RecordList } from "./record";
|
||||
|
||||
export class Store extends Record {
|
||||
/** @returns {import("models").Store} */
|
||||
@@ -153,34 +153,15 @@ export const storeService = {
|
||||
[OgClass.name]: class extends OgClass {
|
||||
constructor() {
|
||||
super();
|
||||
for (const name of Model.__rels__.keys()) {
|
||||
// Relational fields contain symbols for detection in original class.
|
||||
// This constructor is called on genuine records:
|
||||
// - 'one' fields => undefined
|
||||
// - 'many' fields => RecordList
|
||||
let newVal;
|
||||
if (this[name]?.[0] === Record.one()[0]) {
|
||||
newVal = undefined;
|
||||
}
|
||||
if (this[name]?.[0] === Record.many()[0]) {
|
||||
newVal = new RecordList();
|
||||
newVal.__store__ = res.store;
|
||||
newVal.name = name;
|
||||
newVal.owner = this;
|
||||
}
|
||||
this.__rels__.set(name, newVal);
|
||||
this.__invs__ = new RecordInverses();
|
||||
this[name] = newVal;
|
||||
}
|
||||
return new Proxy(this, {
|
||||
const proxy = new Proxy(this, {
|
||||
/** @param {Record} receiver */
|
||||
get(target, name, receiver) {
|
||||
if (name !== "__rels__" && receiver.__rels__.has(name)) {
|
||||
const l1 = receiver.__rels__.get(name);
|
||||
if (l1 instanceof RecordList) {
|
||||
if (RecordList.isMany(l1)) {
|
||||
return l1;
|
||||
}
|
||||
return res.store.get(l1);
|
||||
return l1[0];
|
||||
}
|
||||
return Reflect.get(target, name, receiver);
|
||||
},
|
||||
@@ -188,13 +169,8 @@ export const storeService = {
|
||||
if (name !== "__rels__" && target.__rels__.has(name)) {
|
||||
const r1 = target;
|
||||
const l1 = r1.__rels__.get(name);
|
||||
const r2 = res.store.get(l1);
|
||||
if (r2) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
const { onDelete } = r1.Model.__rels__.get(name);
|
||||
onDelete?.call(r1, r2);
|
||||
}
|
||||
r1.__rels__.set(name, undefined);
|
||||
l1.clear();
|
||||
return true;
|
||||
}
|
||||
const ret = Reflect.deleteProperty(target, name);
|
||||
return ret;
|
||||
@@ -205,18 +181,19 @@ export const storeService = {
|
||||
Reflect.set(target, name, val, receiver);
|
||||
return true;
|
||||
}
|
||||
const oldVal = receiver.__rels__.get(name);
|
||||
if (oldVal instanceof RecordList) {
|
||||
/** @type {RecordList<Record>} */
|
||||
const r1 = receiver;
|
||||
const l1 = receiver.__rels__.get(name);
|
||||
if (RecordList.isMany(l1)) {
|
||||
// [Record.many] =
|
||||
const r1 = receiver;
|
||||
/** @type {RecordList<Record>} */
|
||||
const l1 = r1.__rels__.get(name);
|
||||
if (Record.isCommand(val)) {
|
||||
for (const [cmd, cmdData] of val) {
|
||||
if (Array.isArray(cmdData)) {
|
||||
for (const item of cmdData) {
|
||||
if (cmd === "ADD") {
|
||||
l1.add(item);
|
||||
} else if (cmd === "ADD.noinv") {
|
||||
l1.__addNoinv(item);
|
||||
} else {
|
||||
l1.delete(item);
|
||||
}
|
||||
@@ -224,6 +201,8 @@ export const storeService = {
|
||||
} else {
|
||||
if (cmd === "ADD") {
|
||||
l1.add(cmdData);
|
||||
} else if (cmd === "ADD.noinv") {
|
||||
l1.__addNoinv(cmdData);
|
||||
} else {
|
||||
l1.delete(cmdData);
|
||||
}
|
||||
@@ -231,44 +210,64 @@ export const storeService = {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if ([null, false, undefined].includes(val)) {
|
||||
l1.clear();
|
||||
return true;
|
||||
}
|
||||
if (!Array.isArray(val)) {
|
||||
val = [val];
|
||||
}
|
||||
/** @type {Record[]|Set<Record>|RecordList<Record>} */
|
||||
const collection = val;
|
||||
const collection = Record.isRecord(val) ? [val] : val;
|
||||
const oldRecords = l1.slice();
|
||||
for (const r2 of oldRecords) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
}
|
||||
l1.clear();
|
||||
if ([null, false, undefined].includes(val)) {
|
||||
return true;
|
||||
}
|
||||
l1.push(...collection);
|
||||
} else {
|
||||
let isAddNoinv = false;
|
||||
// [Record.one] =
|
||||
const r1 = receiver;
|
||||
const l1 = r1.__rels__.get(name);
|
||||
const r2 = res.store.get(l1);
|
||||
if (r2) {
|
||||
r2.__invs__.delete(r1.localId, name);
|
||||
const { onDelete } = r1.Model.__rels__.get(name);
|
||||
onDelete?.call(r1, r2);
|
||||
}
|
||||
if (Record.isCommand(val)) {
|
||||
const [cmd, cmdData] = val.at(-1);
|
||||
val = cmd === "ADD" ? cmdData : null;
|
||||
isAddNoinv = cmd === "ADD.noinv";
|
||||
val = ["ADD", "ADD.noinv"].includes(cmd) ? cmdData : null;
|
||||
}
|
||||
if ([null, false, undefined].includes(val)) {
|
||||
delete receiver[name];
|
||||
return true;
|
||||
}
|
||||
const r = preinsert(val, r1, name, (r3) => {
|
||||
r1.__rels__.set(name, r3?.localId);
|
||||
});
|
||||
const { onAdd } = r1.Model.__rels__.get(name);
|
||||
onAdd?.call(r1, r);
|
||||
if (isAddNoinv) {
|
||||
l1.__addNoinv(val);
|
||||
} else {
|
||||
l1.add(val);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (this instanceof Store) {
|
||||
res.store = proxy;
|
||||
}
|
||||
for (const name of Model.__rels__.keys()) {
|
||||
// Relational fields contain symbols for detection in original class.
|
||||
// This constructor is called on genuine records:
|
||||
// - 'one' fields => undefined
|
||||
// - 'many' fields => RecordList
|
||||
// this[name]?.[0] is ONE_SYM or MANY_SYM
|
||||
const newVal = new RecordList(this[name]?.[0]);
|
||||
if (this instanceof Store) {
|
||||
newVal.__store__ = proxy;
|
||||
} else {
|
||||
newVal.__store__ = res.store;
|
||||
}
|
||||
newVal.name = name;
|
||||
newVal.owner = this;
|
||||
this.__rels__.set(name, newVal);
|
||||
this.__invs__ = new RecordInverses();
|
||||
this[name] = newVal;
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
},
|
||||
}[OgClass.name];
|
||||
@@ -282,10 +281,33 @@ export const storeService = {
|
||||
// Detect relational fields with a dummy record and setup getter/setters on them
|
||||
const obj = new OgClass();
|
||||
for (const [name, val] of Object.entries(obj)) {
|
||||
if (![Record.one()[0], Record.many()[0]].includes(val?.[0])) {
|
||||
const SYM = val?.[0];
|
||||
if (![Record.one()[0], Record.many()[0]].includes(SYM)) {
|
||||
continue;
|
||||
}
|
||||
Model.__rels__.set(name, val[1]);
|
||||
Model.__rels__.set(name, { [SYM]: true, ...val[1] });
|
||||
}
|
||||
}
|
||||
// Sync inverse fields
|
||||
for (const Model of Object.values(Models)) {
|
||||
for (const [name, { targetModel, inverse }] of Model.__rels__.entries()) {
|
||||
if (targetModel && !Models[targetModel]) {
|
||||
throw new Error(`No target model ${targetModel} exists`);
|
||||
}
|
||||
if (inverse) {
|
||||
const rel2 = Models[targetModel].__rels__.get(inverse);
|
||||
if (rel2.targetModel && rel2.targetModel !== Model.name) {
|
||||
throw new Error(
|
||||
`Fields ${Models[targetModel].name}.${inverse} has wrong targetModel. Expected: "${Model.name}" Actual: "${rel2.targetModel}"`
|
||||
);
|
||||
}
|
||||
if (rel2.inverse && rel2.inverse !== name) {
|
||||
throw new Error(
|
||||
`Fields ${Models[targetModel].name}.${inverse} has wrong inverse. Expected: "${name}" Actual: "${rel2.inverse}"`
|
||||
);
|
||||
}
|
||||
Object.assign(rel2, { targetModel: Model.name, inverse: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make true store (as a model)
|
||||
|
||||
@@ -50,12 +50,7 @@ export class SuggestionService {
|
||||
[],
|
||||
kwargs
|
||||
);
|
||||
suggestedPartners.map((data) => {
|
||||
this.store.Persona.insert({ ...data, type: "partner" });
|
||||
if (data.persona?.channelMembers) {
|
||||
this.store.ChannelMember.insert(...data.persona.channelMembers);
|
||||
}
|
||||
});
|
||||
suggestedPartners.forEach((data) => this.store.Persona.insert(data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,12 +63,7 @@ export class SuggestionService {
|
||||
[],
|
||||
{ search: term }
|
||||
);
|
||||
suggestedThreads.map((data) => {
|
||||
this.store.Thread.insert({
|
||||
model: "discuss.channel",
|
||||
...data,
|
||||
});
|
||||
});
|
||||
suggestedThreads.forEach((data) => this.store.Thread.insert(data));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -157,7 +157,7 @@ export class Thread extends Component {
|
||||
});
|
||||
useBus(this.env.bus, "MAIL:RELOAD-THREAD", ({ detail }) => {
|
||||
const { model, id } = this.props.thread;
|
||||
if (detail.resModel === model && detail.resId === id) {
|
||||
if (detail.model === model && detail.id === id) {
|
||||
this.threadService.fetchNewMessages(this.props.thread);
|
||||
}
|
||||
});
|
||||
@@ -208,8 +208,8 @@ export class Thread extends Component {
|
||||
await this.env.messageHighlight?.highlightMessage(
|
||||
this.store.Message.insert({
|
||||
id: Number(oeId),
|
||||
resId: this.props.thread.id,
|
||||
resModel: this.props.thread.model,
|
||||
res_id: this.props.thread.id,
|
||||
model: this.props.thread.model,
|
||||
}),
|
||||
this.props.thread
|
||||
);
|
||||
@@ -227,7 +227,7 @@ export class Thread extends Component {
|
||||
if (!msg.author?.eq(prevMsg.author)) {
|
||||
return false;
|
||||
}
|
||||
if (msg.resModel !== prevMsg.resModel || msg.resId !== prevMsg.resId) {
|
||||
if (msg.model !== prevMsg.model || msg.res_id !== prevMsg.res_id) {
|
||||
return false;
|
||||
}
|
||||
if (msg.parentMessage) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { AND, Record } from "@mail/core/common/record";
|
||||
import { ScrollPosition } from "@mail/core/common/scroll_position";
|
||||
import { assignDefined, onChange } from "@mail/utils/common/misc";
|
||||
import { assignDefined, assignIn, onChange } from "@mail/utils/common/misc";
|
||||
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
@@ -32,7 +32,14 @@ export class Thread extends Record {
|
||||
static new(data) {
|
||||
/** @type {import("models").Thread} */
|
||||
const thread = super.new(data);
|
||||
thread.composer = { thread };
|
||||
thread.composer = {};
|
||||
onChange(thread, "isLoaded", () => thread.isLoadedDeferred.resolve());
|
||||
onChange(thread, "channelMembers", () => this.store.updateBusSubscription());
|
||||
onChange(thread, "is_pinned", () => {
|
||||
if (!thread.is_pinned && thread.eq(this.store.discuss.thread)) {
|
||||
this.store.discuss.thread = undefined;
|
||||
}
|
||||
});
|
||||
return thread;
|
||||
}
|
||||
/**
|
||||
@@ -46,10 +53,7 @@ export class Thread extends Record {
|
||||
// Transform "Thread,<model> AND <id>" to "<model>_<id>""
|
||||
return localId.split(",").slice(1).join("_").replace(" AND ", "_");
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {import("models").Thread}
|
||||
*/
|
||||
/** @returns {import("models").Thread} */
|
||||
static insert(data) {
|
||||
if (!("id" in data)) {
|
||||
throw new Error("Cannot insert thread: id is missing in data");
|
||||
@@ -57,42 +61,22 @@ export class Thread extends Record {
|
||||
if (!("model" in data)) {
|
||||
throw new Error("Cannot insert thread: model is missing in data");
|
||||
}
|
||||
let thread = this.get(data);
|
||||
if (thread) {
|
||||
thread.update(data);
|
||||
return thread;
|
||||
}
|
||||
/** @type {import("models").Thread} */
|
||||
thread = this.preinsert(data);
|
||||
thread.type = data.type;
|
||||
onChange(thread, "message_unread_counter", () => {
|
||||
if (thread.channel) {
|
||||
thread.channel.message_unread_counter = thread.message_unread_counter;
|
||||
}
|
||||
});
|
||||
onChange(thread, "isLoaded", () => thread.isLoadedDeferred.resolve());
|
||||
onChange(thread, "channelMembers", () => this.store.updateBusSubscription());
|
||||
onChange(thread, "is_pinned", () => {
|
||||
if (!thread.is_pinned && thread.eq(this.store.discuss.thread)) {
|
||||
this.store.discuss.thread = undefined;
|
||||
}
|
||||
});
|
||||
thread.update(data);
|
||||
// return reactive version.
|
||||
return thread;
|
||||
return super.insert(data);
|
||||
}
|
||||
|
||||
/** @param {Object} data */
|
||||
update(data) {
|
||||
const { id, name, attachments: attachmentsData, description, ...serverData } = data;
|
||||
const { id, name, attachments, description, ...serverData } = data;
|
||||
assignDefined(this, { id, name, description });
|
||||
if (attachmentsData) {
|
||||
this.attachments = attachmentsData.sort((a1, a2) => a2.id - a1.id);
|
||||
if (attachments) {
|
||||
this.attachments = attachments;
|
||||
this.attachments.sort((a1, a2) => a2.id - a1.id);
|
||||
}
|
||||
if (serverData) {
|
||||
assignDefined(this, serverData, [
|
||||
"uuid",
|
||||
"authorizedGroupFullName",
|
||||
"avatarCacheKey",
|
||||
"description",
|
||||
"hasWriteAccess",
|
||||
"is_pinned",
|
||||
@@ -111,23 +95,16 @@ export class Thread extends Record {
|
||||
"is_editable",
|
||||
"defaultDisplayMode",
|
||||
]);
|
||||
if (serverData.channel && "message_unread_counter" in serverData.channel) {
|
||||
this.message_unread_counter = serverData.channel.message_unread_counter;
|
||||
assignIn(this, data, [
|
||||
"custom_channel_name",
|
||||
"memberCount",
|
||||
"channelMembers",
|
||||
"invitedMembers",
|
||||
]);
|
||||
if ("channel_type" in data) {
|
||||
this.type = data.channel_type;
|
||||
}
|
||||
const lastServerMessageId = serverData.last_message_id ?? this.lastServerMessage?.id;
|
||||
if (this.lastServerMessage?.id !== lastServerMessageId) {
|
||||
this.lastServerMessage = { id: lastServerMessageId };
|
||||
}
|
||||
if (this.model === "discuss.channel" && serverData.channel) {
|
||||
this.channel = assignDefined(this.channel ?? {}, serverData.channel);
|
||||
}
|
||||
|
||||
this.memberCount = serverData.channel?.memberCount ?? this.memberCount;
|
||||
if (this.type === "chat" && serverData.channel) {
|
||||
this.customName = serverData.channel.custom_channel_name;
|
||||
}
|
||||
if (serverData.channel?.channelMembers) {
|
||||
this.channelMembers = serverData.channel.channelMembers;
|
||||
if ("channelMembers" in data) {
|
||||
if (this.type === "chat") {
|
||||
for (const member of this.channelMembers) {
|
||||
if (
|
||||
@@ -140,9 +117,6 @@ export class Thread extends Record {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("invitedMembers" in serverData) {
|
||||
this.invitedMembers = serverData.invitedMembers;
|
||||
}
|
||||
if ("seen_partners_info" in serverData) {
|
||||
this.seenInfos = serverData.seen_partners_info.map(
|
||||
({ fetched_message_id, partner_id, seen_message_id }) => {
|
||||
@@ -199,10 +173,10 @@ export class Thread extends Record {
|
||||
});
|
||||
invitedMembers = Record.many("ChannelMember");
|
||||
chatPartner = Record.one("Persona");
|
||||
composer = Record.one("Composer", { onDelete: (r) => r.delete() });
|
||||
composer = Record.one("Composer", { inverse: "thread", onDelete: (r) => r.delete() });
|
||||
counter = 0;
|
||||
/** @type {string} */
|
||||
customName;
|
||||
custom_channel_name;
|
||||
/** @type {string} */
|
||||
description;
|
||||
followers = Record.many("Follower");
|
||||
@@ -274,7 +248,6 @@ export class Thread extends Record {
|
||||
canPostOnReadonly;
|
||||
/** @type {String} */
|
||||
last_interest_dt;
|
||||
lastServerMessage = Record.one("Message");
|
||||
/** @type {Boolean} */
|
||||
is_editable;
|
||||
|
||||
@@ -337,7 +310,7 @@ export class Thread extends Record {
|
||||
|
||||
get displayName() {
|
||||
if (this.type === "chat" && this.chatPartner) {
|
||||
return this.customName || this.chatPartner.nameOrDisplayName;
|
||||
return this.custom_channel_name || this.chatPartner.nameOrDisplayName;
|
||||
}
|
||||
if (this.type === "group" && !this.name) {
|
||||
const listFormatter = new Intl.ListFormat(
|
||||
|
||||
@@ -52,7 +52,7 @@ export class ThreadService {
|
||||
return this.store.Thread.insert({
|
||||
...channelData,
|
||||
model: "discuss.channel",
|
||||
type: channelData.channel.channel_type,
|
||||
type: channelData.channel_type,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export class ThreadService {
|
||||
newUnreadCounter++;
|
||||
}
|
||||
}
|
||||
thread.update({
|
||||
Object.assign(thread, {
|
||||
seen_message_id: lastSeenId,
|
||||
message_needaction_counter: newNeedactionCounter,
|
||||
message_unread_counter: newUnreadCounter,
|
||||
@@ -202,7 +202,7 @@ export class ThreadService {
|
||||
Object.assign(data, { body: data.body ? markup(data.body) : data.body })
|
||||
);
|
||||
});
|
||||
thread.update({ isLoaded: true });
|
||||
thread.isLoaded = true;
|
||||
return messages;
|
||||
} catch (e) {
|
||||
thread.hasLoadingFailed = true;
|
||||
@@ -605,7 +605,7 @@ export class ThreadService {
|
||||
name: newName,
|
||||
});
|
||||
} else if (thread.type === "chat") {
|
||||
thread.customName = newName;
|
||||
thread.custom_channel_name = newName;
|
||||
await this.orm.call("discuss.channel", "channel_set_custom_name", [[thread.id]], {
|
||||
name: newName,
|
||||
});
|
||||
|
||||
@@ -31,8 +31,8 @@ export class ActivityMarkAsDone extends Component {
|
||||
}
|
||||
|
||||
async onClickDone() {
|
||||
const { res_id: resId, res_model: resModel } = this.props.activity;
|
||||
const thread = this.threadService.getThread(resModel, resId);
|
||||
const { res_id, res_model } = this.props.activity;
|
||||
const thread = this.threadService.getThread(res_model, res_id);
|
||||
await this.env.services["mail.activity"].markAsDone(this.props.activity);
|
||||
if (this.props.reload) {
|
||||
this.props.reload(this.props.activity.res_id, ["activities"]);
|
||||
@@ -41,8 +41,8 @@ export class ActivityMarkAsDone extends Component {
|
||||
}
|
||||
|
||||
async onClickDoneAndScheduleNext() {
|
||||
const { res_id: resId, res_model: resModel } = this.props.activity;
|
||||
const thread = this.threadService.getThread(resModel, resId);
|
||||
const { res_id, res_model } = this.props.activity;
|
||||
const thread = this.threadService.getThread(res_model, res_id);
|
||||
if (this.props.onClickDoneAndScheduleNext) {
|
||||
this.props.onClickDoneAndScheduleNext();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ActivityService {
|
||||
});
|
||||
this.broadcastChannel?.postMessage({
|
||||
type: "RELOAD_CHATTER",
|
||||
payload: { resId: activity.res_id, resModel: activity.res_model },
|
||||
payload: { id: activity.res_id, model: activity.res_model },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ActivityService {
|
||||
);
|
||||
this.broadcastChannel?.postMessage({
|
||||
type: "RELOAD_CHATTER",
|
||||
payload: { resId: activity.res_id, resModel: activity.res_model },
|
||||
payload: { id: activity.res_id, model: activity.res_model },
|
||||
});
|
||||
return action;
|
||||
}
|
||||
@@ -114,8 +114,8 @@ export class ActivityService {
|
||||
}
|
||||
case "RELOAD_CHATTER": {
|
||||
const thread = this.env.services["mail.thread"].getThread(
|
||||
data.payload.resModel,
|
||||
data.payload.resId
|
||||
data.payload.model,
|
||||
data.payload.id
|
||||
);
|
||||
this.env.services["mail.thread"].fetchNewMessages(thread);
|
||||
break;
|
||||
@@ -124,12 +124,8 @@ export class ActivityService {
|
||||
}
|
||||
|
||||
_serialize(activity) {
|
||||
const data = { ...activity };
|
||||
delete data._store;
|
||||
delete data.__rels__;
|
||||
delete data.__invs__;
|
||||
delete data.Model;
|
||||
return JSON.parse(JSON.stringify(data));
|
||||
activity = activity.toData();
|
||||
return JSON.parse(JSON.stringify(activity));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.Message" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o-mail-Message-textContent')]/*[1]" position="before">
|
||||
<t t-if="message.type === 'notification' or message.isTransient or message.trackingValues.length > 0" name="hasTrackingValue">
|
||||
<t t-if="message.type === 'notification' or message.is_transient or message.trackingValues.length > 0" name="hasTrackingValue">
|
||||
<div>
|
||||
<t t-if="message.subtypeDescription">
|
||||
<t t-if="message.subtype_description">
|
||||
<p class="mb-0">
|
||||
<t t-out="messageSearch?.highlight(message.subtypeDescription) ?? message.subtypeDescription"/>
|
||||
<t t-out="messageSearch?.highlight(message.subtype_description) ?? message.subtype_description"/>
|
||||
</p>
|
||||
</t>
|
||||
<t t-if="message.trackingValues.length">
|
||||
|
||||
@@ -63,7 +63,7 @@ patch(ThreadService.prototype, {
|
||||
}
|
||||
}
|
||||
if ("attachments" in result) {
|
||||
thread.update({
|
||||
Object.assign(thread, {
|
||||
areAttachmentsLoaded: true,
|
||||
attachments: result.attachments,
|
||||
isLoadingAttachments: false,
|
||||
@@ -99,21 +99,17 @@ patch(ThreadService.prototype, {
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getThread(resModel, resId) {
|
||||
let thread = this.store.Thread.get({ model: resModel, id: resId });
|
||||
getThread(model, id) {
|
||||
let thread = this.store.Thread.get({ model, id });
|
||||
if (thread) {
|
||||
if (resId === false) {
|
||||
if (id === false) {
|
||||
return thread;
|
||||
}
|
||||
// to force a reload
|
||||
thread.status = "new";
|
||||
}
|
||||
thread = this.store.Thread.insert({
|
||||
id: resId,
|
||||
model: resModel,
|
||||
type: "chatter",
|
||||
});
|
||||
if (resId === false) {
|
||||
thread = this.store.Thread.insert({ id, model, type: "chatter" });
|
||||
if (id === false) {
|
||||
thread.messages.push({
|
||||
id: this.messageService.getNextTemporaryId(),
|
||||
author: { id: this.store.self.id },
|
||||
|
||||
@@ -141,7 +141,7 @@ export class CallParticipantCard extends Component {
|
||||
channel_id: this.props.thread.id,
|
||||
member_ids: [this.channelMember.id],
|
||||
});
|
||||
this.props.thread.update({ invitedMembers: channelData.invitedMembers });
|
||||
this.props.thread.invitedMembers = channelData.invitedMembers;
|
||||
}
|
||||
|
||||
async onClickReplay() {
|
||||
|
||||
@@ -715,7 +715,7 @@ export class Rtc {
|
||||
this.clear();
|
||||
this.state.logs.clear();
|
||||
this.state.channel = channel;
|
||||
this.state.channel.update({ rtcSessions });
|
||||
this.state.channel.rtcSessions = rtcSessions;
|
||||
this.state.selfSession = this.store.RtcSession.get(sessionId);
|
||||
this.state.iceServers = iceServers || DEFAULT_ICE_SERVERS;
|
||||
this.state.logs.set("channelId", this.state.channel?.id);
|
||||
@@ -1577,14 +1577,15 @@ export const rtcService = {
|
||||
services["bus_service"].subscribe("discuss.channel/joined", ({ channel }) => {
|
||||
rtc.updateRtcSessions(channel.id, channel.rtcSessions);
|
||||
});
|
||||
services["bus_service"].subscribe("res.users.settings.volumes", (payload) => {
|
||||
if (payload) {
|
||||
services["mail.user_settings"].setVolumes(payload);
|
||||
}
|
||||
});
|
||||
services["bus_service"].subscribe("mail.record/insert", (payload) => {
|
||||
if (payload.RtcSession) {
|
||||
rtc.store.RtcSession.insert(payload.RtcSession);
|
||||
}
|
||||
const { "res.users.settings.volumes": volumeSettings } = payload;
|
||||
if (volumeSettings) {
|
||||
services["mail.user_settings"].setVolumes(volumeSettings);
|
||||
}
|
||||
});
|
||||
return rtc;
|
||||
},
|
||||
|
||||
@@ -10,32 +10,16 @@ export class RtcSession extends Record {
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/**
|
||||
* @param {Object} data
|
||||
* @returns {number, import("models").RtcSession}
|
||||
*/
|
||||
/** @returns {number, import("models").RtcSession} */
|
||||
static insert(data) {
|
||||
/** @type {import("models").RtcSession} */
|
||||
const session = this.preinsert(data);
|
||||
const { channelMember, ...remainingData } = data;
|
||||
for (const key in remainingData) {
|
||||
session[key] = remainingData[key];
|
||||
}
|
||||
if (channelMember?.channel) {
|
||||
session.channelId = channelMember.channel.id;
|
||||
}
|
||||
if (channelMember) {
|
||||
const channelMemberRecord = this.store.ChannelMember.insert(channelMember);
|
||||
channelMemberRecord.rtcSession = session;
|
||||
session.channelMemberId = channelMemberRecord.id;
|
||||
channelMemberRecord.thread?.rtcSessions.add(session);
|
||||
}
|
||||
const session = super.insert(data);
|
||||
session.channel?.rtcSessions.add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
// Server data
|
||||
channelId;
|
||||
channelMemberId;
|
||||
channelMember = Record.one("ChannelMember", { inverse: "rtcSession" });
|
||||
isCameraOn;
|
||||
id;
|
||||
isDeaf;
|
||||
@@ -73,12 +57,8 @@ export class RtcSession extends Record {
|
||||
iceGatheringState;
|
||||
logStep;
|
||||
|
||||
get channelMember() {
|
||||
return this._store.ChannelMember.get(this.channelMemberId);
|
||||
}
|
||||
|
||||
get channel() {
|
||||
return this._store.Thread.get({ model: "discuss.channel", id: this.channelId });
|
||||
return this.channelMember?.thread;
|
||||
}
|
||||
|
||||
get isMute() {
|
||||
|
||||
@@ -35,8 +35,7 @@ export class DiscussCoreCommon {
|
||||
const thread = this.store.Thread.insert({
|
||||
...channel,
|
||||
model: "discuss.channel",
|
||||
channel: channel.channel,
|
||||
type: channel.channel.channel_type,
|
||||
type: channel.channel_type,
|
||||
});
|
||||
if (invitedByUserId && invitedByUserId !== this.store.user?.user?.id) {
|
||||
this.notificationService.add(
|
||||
@@ -49,7 +48,7 @@ export class DiscussCoreCommon {
|
||||
const { id, last_interest_dt } = payload;
|
||||
const channel = this.store.Thread.get({ model: "discuss.channel", id });
|
||||
if (channel) {
|
||||
channel.update({ last_interest_dt });
|
||||
channel.last_interest_dt = last_interest_dt;
|
||||
if (channel.type !== "channel") {
|
||||
this.threadService.sortChannels();
|
||||
}
|
||||
@@ -65,14 +64,6 @@ export class DiscussCoreCommon {
|
||||
});
|
||||
thread.delete();
|
||||
});
|
||||
this.busService.subscribe("discuss.channel/legacy_insert", (payload) => {
|
||||
this.store.Thread.insert({
|
||||
id: payload.channel.id,
|
||||
model: "discuss.channel",
|
||||
type: payload.channel.channel_type,
|
||||
...payload,
|
||||
});
|
||||
});
|
||||
this.busService.addEventListener("notification", ({ detail: notifications }) => {
|
||||
// Do not handle new message notification if the channel was just left. This issue
|
||||
// occurs because the "discuss.channel/leave" and the "discuss.channel/new_message"
|
||||
@@ -147,15 +138,6 @@ export class DiscussCoreCommon {
|
||||
}
|
||||
}
|
||||
});
|
||||
this.busService.subscribe("mail.record/insert", (payload) => {
|
||||
if (payload.Channel) {
|
||||
this.store.Thread.insert({
|
||||
id: payload.Channel.id,
|
||||
model: "discuss.channel",
|
||||
channel: payload.Channel,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,9 +150,9 @@ export class DiscussCoreCommon {
|
||||
const thread = this.store.Thread.insert({
|
||||
...serverData,
|
||||
model: "discuss.channel",
|
||||
type: serverData.channel.channel_type,
|
||||
type: serverData.channel_type,
|
||||
isAdmin:
|
||||
serverData.channel.channel_type !== "group" &&
|
||||
serverData.channel_type !== "group" &&
|
||||
serverData.create_uid === this.store.user?.user?.id,
|
||||
});
|
||||
return thread;
|
||||
@@ -214,7 +196,7 @@ export class DiscussCoreCommon {
|
||||
const [channelData] = await this.rpc("/discuss/channel/info", { channel_id: id });
|
||||
channel = this.store.Thread.insert({
|
||||
model: "discuss.channel",
|
||||
type: channelData.channel.channel_type,
|
||||
type: channelData.channel_type,
|
||||
...channelData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ patch(Thread.prototype, {
|
||||
if (this.type === "channel" || this.type === "group") {
|
||||
return url(
|
||||
`/discuss/channel/${this.id}/avatar_128`,
|
||||
assignDefined({}, { unique: this.channel?.avatarCacheKey })
|
||||
assignDefined({}, { unique: this.avatarCacheKey })
|
||||
);
|
||||
}
|
||||
if (this.type === "chat") {
|
||||
|
||||
@@ -45,7 +45,7 @@ export class DiscussPublic extends Component {
|
||||
return this.store.Thread.insert({
|
||||
id: this.props.data.channelData.id,
|
||||
model: "discuss.channel",
|
||||
type: this.props.data.channelData.channel.channel_type,
|
||||
type: this.props.data.channelData.channel_type,
|
||||
uuid: this.props.data.channelData.uuid,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,14 +58,13 @@ export class DiscussCoreWeb {
|
||||
this.store.ChatWindow.insert({ thread: channel });
|
||||
}
|
||||
);
|
||||
this.busService.subscribe("mail.record/insert", (payload) => {
|
||||
const { "res.users.settings": settings } = payload;
|
||||
if (settings) {
|
||||
this.busService.subscribe("res.users.settings", (payload) => {
|
||||
if (payload) {
|
||||
this.store.discuss.chats.isOpen =
|
||||
settings.is_discuss_sidebar_category_chat_open ??
|
||||
payload.is_discuss_sidebar_category_chat_open ??
|
||||
this.store.discuss.chats.isOpen;
|
||||
this.store.discuss.channels.isOpen =
|
||||
settings.is_discuss_sidebar_category_channel_open ??
|
||||
payload.is_discuss_sidebar_category_channel_open ??
|
||||
this.store.discuss.channels.isOpen;
|
||||
}
|
||||
});
|
||||
@@ -83,27 +82,24 @@ export class DiscussCoreWeb {
|
||||
this.store.ChatWindow.insert({ thread: chat });
|
||||
}
|
||||
});
|
||||
this.busService.subscribe("mail.record/insert", (payload) => {
|
||||
if (payload.Thread) {
|
||||
const data = payload.Thread;
|
||||
const thread = this.store.Thread.get(data);
|
||||
if (data.fold_state && thread && data.foldStateCount > thread.foldStateCount) {
|
||||
thread.foldStateCount = data.foldStateCount;
|
||||
if (data.fold_state !== thread.state) {
|
||||
thread.state = data.fold_state;
|
||||
if (thread.state === "closed") {
|
||||
const chatWindow = this.store.discuss.chatWindows.find((chatWindow) =>
|
||||
chatWindow.thread?.eq(thread)
|
||||
);
|
||||
if (chatWindow) {
|
||||
this.chatWindowService.close(chatWindow, { notifyState: false });
|
||||
}
|
||||
} else {
|
||||
this.store.ChatWindow.insert({
|
||||
thread,
|
||||
folded: thread.state === "folded",
|
||||
});
|
||||
this.busService.subscribe("discuss.Thread/fold_state", (data) => {
|
||||
const thread = this.store.Thread.get(data);
|
||||
if (data.fold_state && thread && data.foldStateCount > thread.foldStateCount) {
|
||||
thread.foldStateCount = data.foldStateCount;
|
||||
if (data.fold_state !== thread.state) {
|
||||
thread.state = data.fold_state;
|
||||
if (thread.state === "closed") {
|
||||
const chatWindow = this.store.discuss.chatWindows.find((chatWindow) =>
|
||||
chatWindow.thread?.eq(thread)
|
||||
);
|
||||
if (chatWindow) {
|
||||
this.chatWindowService.close(chatWindow, { notifyState: false });
|
||||
}
|
||||
} else {
|
||||
this.store.ChatWindow.insert({
|
||||
thread,
|
||||
folded: thread.state === "folded",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ export function assignDefined(obj, data, keys = Object.keys(data)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function assignIn(obj, data, keys = Object.keys(data)) {
|
||||
for (const key of keys) {
|
||||
if (key in data) {
|
||||
obj[key] = data[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// todo: move this some other place in the future
|
||||
export function isDragSourceExternalFile(dataTransfer) {
|
||||
const dragDataType = dataTransfer.types;
|
||||
|
||||
@@ -13,7 +13,7 @@ patch(FormController.prototype, {
|
||||
if (isSameThread) {
|
||||
// not first load
|
||||
const { resModel, resId } = this.model.root;
|
||||
this.env.bus.trigger("MAIL:RELOAD-THREAD", { resModel, resId });
|
||||
this.env.bus.trigger("MAIL:RELOAD-THREAD", { model: resModel, id: resId });
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ QUnit.test("Message model properties", async (assert) => {
|
||||
});
|
||||
/** @type {import("models").Message} */
|
||||
const message = env.services["mail.store"].Message.insert({
|
||||
attachment_ids: [
|
||||
attachments: [
|
||||
{
|
||||
filename: "test.txt",
|
||||
id: 750,
|
||||
@@ -29,8 +29,8 @@ QUnit.test("Message model properties", async (assert) => {
|
||||
needaction_partner_ids: [3],
|
||||
starred_partner_ids: [3],
|
||||
isStarred: true,
|
||||
resModel: "res.partner",
|
||||
resId: 3,
|
||||
model: "res.partner",
|
||||
res_id: 3,
|
||||
});
|
||||
assert.ok(message);
|
||||
assert.ok(message.isNeedaction);
|
||||
|
||||
@@ -808,11 +808,9 @@ QUnit.test("channel - states: close from the bus", async () => {
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_channel_open: false,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_channel_open: false,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-right");
|
||||
await contains("button", { count: 0, text: "channel1" });
|
||||
@@ -827,11 +825,9 @@ QUnit.test("channel - states: open from the bus", async () => {
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_channel_open: true,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_channel_open: true,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-channel .oi-chevron-down");
|
||||
await contains("button", { text: "channel1" });
|
||||
@@ -932,11 +928,9 @@ QUnit.test("chat - states: close from the bus", async () => {
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_chat_open: false,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_chat_open: false,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-chat .oi-chevron-right");
|
||||
await contains(".o-mail-DiscussSidebar button", { count: 0, text: "Mitchell Admin" });
|
||||
@@ -951,11 +945,9 @@ QUnit.test("chat - states: open from the bus", async () => {
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss();
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", {
|
||||
"res.users.settings": {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_chat_open: true,
|
||||
},
|
||||
pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "res.users.settings", {
|
||||
id: userSettingsId,
|
||||
is_discuss_sidebar_category_chat_open: true,
|
||||
});
|
||||
await contains(".o-mail-DiscussSidebarCategory-chat .oi-chevron-down");
|
||||
await contains(".o-mail-DiscussSidebar button", { text: "Mitchell Admin" });
|
||||
|
||||
@@ -137,9 +137,7 @@ patch(MockServer.prototype, {
|
||||
Message: {
|
||||
id: args.message_id,
|
||||
body: args.body,
|
||||
attachment_ids: this._mockIrAttachment_attachmentFormat(
|
||||
args.attachment_ids
|
||||
),
|
||||
attachments: this._mockIrAttachment_attachmentFormat(args.attachment_ids),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -265,12 +265,13 @@ patch(MockServer.prototype, {
|
||||
id: channel.id,
|
||||
});
|
||||
this.pyEnv["bus.bus"]._sendone(channel, "mail.record/insert", {
|
||||
Channel: {
|
||||
Thread: {
|
||||
id: channel.id,
|
||||
channelMembers: [["DELETE", { id: channelMember.id }]],
|
||||
memberCount: this.pyEnv["discuss.channel.member"].searchCount([
|
||||
["channel_id", "=", channel.id],
|
||||
]),
|
||||
model: "discuss.channel",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -336,7 +337,7 @@ patch(MockServer.prototype, {
|
||||
]) > 0;
|
||||
if (isSelfMember) {
|
||||
this.pyEnv["bus.bus"]._sendone(channel, "mail.record/insert", {
|
||||
Channel: {
|
||||
Thread: {
|
||||
id: channel.id,
|
||||
channelMembers: [
|
||||
[
|
||||
@@ -349,6 +350,7 @@ patch(MockServer.prototype, {
|
||||
memberCount: this.pyEnv["discuss.channel.member"].searchCount([
|
||||
["channel_id", "=", channel.id],
|
||||
]),
|
||||
model: "discuss.channel",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -420,7 +422,7 @@ patch(MockServer.prototype, {
|
||||
["id", "=", partner_id],
|
||||
]);
|
||||
for (const channelInfo of channelInfos) {
|
||||
notifications.push([relatedPartner, "discuss.channel/legacy_insert", channelInfo]);
|
||||
notifications.push([relatedPartner, "mail.record/insert", { Thread: channelInfo }]);
|
||||
}
|
||||
}
|
||||
return notifications;
|
||||
@@ -517,13 +519,11 @@ patch(MockServer.prototype, {
|
||||
is_minimized: foldState !== "closed",
|
||||
};
|
||||
this.pyEnv["discuss.channel.member"].write([memberOfCurrentUser.id], vals);
|
||||
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", {
|
||||
Thread: {
|
||||
foldStateCount: state_count,
|
||||
id: channel.id,
|
||||
model: "discuss.channel",
|
||||
fold_state: foldState,
|
||||
},
|
||||
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "discuss.Thread/fold_state", {
|
||||
foldStateCount: state_count,
|
||||
id: channel.id,
|
||||
model: "discuss.channel",
|
||||
fold_state: foldState,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -626,25 +626,11 @@ patch(MockServer.prototype, {
|
||||
const [group_public_id] = this.getRecords("res.groups", [
|
||||
["id", "=", channel.group_public_id],
|
||||
]);
|
||||
const lastMessageId = messages.reduce((lastMessageId, message) => {
|
||||
if (!lastMessageId || message.id > lastMessageId) {
|
||||
return message.id;
|
||||
}
|
||||
return lastMessageId;
|
||||
}, undefined);
|
||||
const messageNeedactionCounter = this.getRecords("mail.notification", [
|
||||
["res_partner_id", "=", this.pyEnv.currentPartnerId],
|
||||
["is_read", "=", false],
|
||||
["mail_message_id", "in", messages.map((message) => message.id)],
|
||||
]).length;
|
||||
const channelData = {
|
||||
avatarCacheKey: channel.avatarCacheKey,
|
||||
channel_type: channel.channel_type,
|
||||
id: channel.id,
|
||||
memberCount: this.pyEnv["discuss.channel.member"].searchCount([
|
||||
["channel_id", "=", channel.id],
|
||||
]),
|
||||
};
|
||||
const res = assignDefined({}, channel, [
|
||||
"id",
|
||||
"name",
|
||||
@@ -656,9 +642,13 @@ patch(MockServer.prototype, {
|
||||
"avatarCacheKey",
|
||||
]);
|
||||
Object.assign(res, {
|
||||
last_message_id: lastMessageId,
|
||||
channel_type: channel.channel_type,
|
||||
memberCount: this.pyEnv["discuss.channel.member"].searchCount([
|
||||
["channel_id", "=", channel.id],
|
||||
]),
|
||||
message_needaction_counter: messageNeedactionCounter,
|
||||
authorizedGroupFullName: group_public_id ? group_public_id.name : false,
|
||||
model: "discuss.channel",
|
||||
});
|
||||
const memberOfCurrentUser = this._mockDiscussChannelMember__getAsSudoFromContext(
|
||||
channel.id
|
||||
@@ -674,7 +664,7 @@ patch(MockServer.prototype, {
|
||||
? memberOfCurrentUser.seen_message_id[0]
|
||||
: memberOfCurrentUser.seen_message_id,
|
||||
});
|
||||
Object.assign(channelData, {
|
||||
Object.assign(res, {
|
||||
custom_channel_name: memberOfCurrentUser.custom_channel_name,
|
||||
message_unread_counter: memberOfCurrentUser.message_unread_counter,
|
||||
});
|
||||
@@ -683,7 +673,7 @@ patch(MockServer.prototype, {
|
||||
id: memberOfCurrentUser.rtc_inviting_session_id,
|
||||
};
|
||||
}
|
||||
channelData["channelMembers"] = [
|
||||
res["channelMembers"] = [
|
||||
[
|
||||
"ADD",
|
||||
this._mockDiscussChannelMember_DiscussChannelMemberFormat([
|
||||
@@ -702,7 +692,7 @@ patch(MockServer.prototype, {
|
||||
fetched_message_id: member.fetched_message_id,
|
||||
};
|
||||
});
|
||||
channelData["channelMembers"] = [
|
||||
res["channelMembers"] = [
|
||||
[
|
||||
"ADD",
|
||||
this._mockDiscussChannelMember_DiscussChannelMemberFormat(
|
||||
@@ -734,7 +724,6 @@ patch(MockServer.prototype, {
|
||||
),
|
||||
],
|
||||
];
|
||||
res.channel = channelData;
|
||||
res.allow_public_upload = channel.allow_public_upload;
|
||||
return res;
|
||||
});
|
||||
@@ -761,11 +750,9 @@ patch(MockServer.prototype, {
|
||||
id: channel.id,
|
||||
});
|
||||
} else {
|
||||
this.pyEnv["bus.bus"]._sendone(
|
||||
this.pyEnv.currentPartner,
|
||||
"discuss.channel/legacy_insert",
|
||||
this._mockDiscussChannelChannelInfo([channel.id])[0]
|
||||
);
|
||||
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", {
|
||||
Thread: this._mockDiscussChannelChannelInfo([channel.id])[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -833,9 +820,10 @@ patch(MockServer.prototype, {
|
||||
custom_channel_name: name,
|
||||
});
|
||||
this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", {
|
||||
Channel: {
|
||||
Thread: {
|
||||
custom_channel_name: name,
|
||||
id: channelId,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -950,11 +938,8 @@ patch(MockServer.prototype, {
|
||||
authorizedGroupFullName: channel.group_public_id
|
||||
? channel.group_public_id.name
|
||||
: false,
|
||||
channel: {
|
||||
channel_type: channel.channel_type,
|
||||
id: channel.id,
|
||||
},
|
||||
id: channel.id,
|
||||
model: "discuss.channel",
|
||||
name: channel.name,
|
||||
};
|
||||
});
|
||||
@@ -982,9 +967,10 @@ patch(MockServer.prototype, {
|
||||
});
|
||||
const channel = this.pyEnv["discuss.channel"].searchRead([["id", "=", id]])[0];
|
||||
this.pyEnv["bus.bus"]._sendone(channel, "mail.record/insert", {
|
||||
Channel: {
|
||||
Thread: {
|
||||
avatarCacheKey: channel.avatarCacheKey,
|
||||
id: id,
|
||||
id,
|
||||
model: "discuss.channel",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ patch(MockServer.prototype, {
|
||||
};
|
||||
}
|
||||
const data = {
|
||||
channel: { id: member.channel_id },
|
||||
thread: { id: member.channel_id, model: "discuss.channel" },
|
||||
id: member.id,
|
||||
persona,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ patch(MockServer.prototype, {
|
||||
_mockMailLinkPreviewFormat(linkPreview) {
|
||||
return {
|
||||
id: linkPreview.id,
|
||||
message_id: linkPreview.message_id[0] || linkPreview.message_id,
|
||||
message: { id: linkPreview.message_id[0] || linkPreview.message_id },
|
||||
image_mimetype: linkPreview.image_mimetype,
|
||||
og_description: linkPreview.og_description,
|
||||
og_image: linkPreview.og_image,
|
||||
|
||||
@@ -69,7 +69,7 @@ patch(MockServer.prototype, {
|
||||
])[0];
|
||||
const result = {
|
||||
id: messageId,
|
||||
messageReactionGroups: [
|
||||
reactions: [
|
||||
[
|
||||
reactions.length > 0 ? "ADD" : "DELETE",
|
||||
{
|
||||
@@ -301,7 +301,7 @@ patch(MockServer.prototype, {
|
||||
});
|
||||
}
|
||||
const response = Object.assign({}, message, {
|
||||
attachment_ids: formattedAttachments,
|
||||
attachments: formattedAttachments,
|
||||
author,
|
||||
history_partner_ids: historyPartnerIds,
|
||||
default_subject:
|
||||
@@ -311,13 +311,13 @@ patch(MockServer.prototype, {
|
||||
message.res_id
|
||||
),
|
||||
linkPreviews: linkPreviewsFormatted,
|
||||
messageReactionGroups: reactionGroups,
|
||||
reactions: reactionGroups,
|
||||
needaction_partner_ids: needactionPartnerIds,
|
||||
notifications,
|
||||
parentMessage: message.parent_id
|
||||
? this._mockMailMessageMessageFormat([message.parent_id])[0]
|
||||
: false,
|
||||
recipients: partners.map((p) => ({ id: p.id, name: p.name })),
|
||||
recipients: partners.map((p) => ({ id: p.id, name: p.name, type: "partner" })),
|
||||
record_name:
|
||||
thread && (thread.name !== undefined ? thread.name : thread.display_name),
|
||||
trackingValues: formattedTrackingValues,
|
||||
|
||||
@@ -146,16 +146,12 @@ patch(MockServer.prototype, {
|
||||
["channel_id", "=", channel_id],
|
||||
["partner_id", "=", partnerFormat.id],
|
||||
]);
|
||||
partnerFormat["persona"] = {
|
||||
channelMembers: [
|
||||
[
|
||||
"ADD",
|
||||
this._mockDiscussChannelMember_DiscussChannelMemberFormat([
|
||||
member.id,
|
||||
])[0],
|
||||
],
|
||||
partnerFormat["channelMembers"] = [
|
||||
[
|
||||
"ADD",
|
||||
this._mockDiscussChannelMember_DiscussChannelMemberFormat([member.id])[0],
|
||||
],
|
||||
};
|
||||
];
|
||||
return partnerFormat;
|
||||
});
|
||||
// reduce results to max limit
|
||||
|
||||
@@ -86,8 +86,9 @@ patch(MockServer.prototype, {
|
||||
const [relatedPartner] = this.pyEnv["res.partner"].searchRead([
|
||||
["id", "=", relatedUser.partner_id[0]],
|
||||
]);
|
||||
this.pyEnv["bus.bus"]._sendone(relatedPartner, "mail.record/insert", {
|
||||
"res.users.settings": { ...changedSettings, id },
|
||||
this.pyEnv["bus.bus"]._sendone(relatedPartner, "res.users.settings", {
|
||||
...changedSettings,
|
||||
id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -327,7 +327,7 @@ class TestChannelInternals(MailCommon):
|
||||
# `channel_get` should return a new channel the first time a partner is given
|
||||
initial_channel_info = self.env['discuss.channel'].channel_get(partners_to=self.test_partner.ids)
|
||||
# shape of channelMembers is [('ADD', data...)], [0][1] accesses the data
|
||||
self.assertEqual(set(m['persona']['id'] for m in initial_channel_info['channel']['channelMembers'][0][1]), {self.partner_employee_nomail.id, self.test_partner.id})
|
||||
self.assertEqual({m['persona']['id'] for m in initial_channel_info['channelMembers'][0][1]}, {self.partner_employee_nomail.id, self.test_partner.id})
|
||||
|
||||
# `channel_get` should return the existing channel every time the same partner is given
|
||||
same_channel_info = self.env['discuss.channel'].channel_get(partners_to=self.test_partner.ids)
|
||||
@@ -342,7 +342,7 @@ class TestChannelInternals(MailCommon):
|
||||
solo_channel_info = self.env['discuss.channel'].channel_get(partners_to=self.partner_employee_nomail.ids)
|
||||
self.assertNotEqual(solo_channel_info['id'], initial_channel_info['id'])
|
||||
# shape of channelMembers is [('ADD', data...)], [0][1] accesses the data
|
||||
self.assertEqual(set(m['persona']['id'] for m in solo_channel_info['channel']['channelMembers'][0][1]), {self.partner_employee_nomail.id})
|
||||
self.assertEqual({m['persona']['id'] for m in solo_channel_info['channelMembers'][0][1]}, {self.partner_employee_nomail.id})
|
||||
|
||||
# `channel_get` should return the existing channel every time the current partner is given
|
||||
same_solo_channel_info = self.env['discuss.channel'].channel_get(partners_to=self.partner_employee_nomail.ids)
|
||||
@@ -535,9 +535,10 @@ class TestChannelInternals(MailCommon):
|
||||
[{
|
||||
"type": "mail.record/insert",
|
||||
"payload": {
|
||||
'Channel': {
|
||||
'Thread': {
|
||||
"avatarCacheKey": avatar_cache_key,
|
||||
"id": channel.id,
|
||||
'model': "discuss.channel",
|
||||
}
|
||||
},
|
||||
}]
|
||||
|
||||
@@ -95,7 +95,7 @@ class TestMessageController(HttpCase):
|
||||
self.assertEqual(res2.status_code, 200)
|
||||
message_format1 = res2.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format1["attachment_ids"],
|
||||
message_format1["attachments"],
|
||||
json.loads(json.dumps(self.attachments[0]._attachment_format(), default=date_utils.json_default)),
|
||||
"guest should be allowed to add attachment with token when posting message",
|
||||
)
|
||||
@@ -138,7 +138,7 @@ class TestMessageController(HttpCase):
|
||||
self.assertEqual(res4.status_code, 200)
|
||||
message_format2 = res4.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format2["attachment_ids"],
|
||||
message_format2["attachments"],
|
||||
json.loads(json.dumps(self.attachments.sorted("id")._attachment_format(), default=date_utils.json_default)),
|
||||
"guest should be allowed to add attachment with token when updating message",
|
||||
)
|
||||
@@ -159,7 +159,7 @@ class TestMessageController(HttpCase):
|
||||
self.assertEqual(res5.status_code, 200)
|
||||
message_format3 = res5.json()["result"]
|
||||
self.assertEqual(
|
||||
message_format3["attachment_ids"],
|
||||
message_format3["attachments"],
|
||||
json.loads(json.dumps(self.attachments.sorted("id")._attachment_format(), default=date_utils.json_default)),
|
||||
"guest should be allowed to add own attachment without token when updating message",
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -74,7 +74,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -122,7 +122,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -145,7 +145,7 @@ class TestChannelInternals(MailCommon):
|
||||
'model': 'discuss.channel',
|
||||
'invitedMembers': [('ADD', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -162,7 +162,7 @@ class TestChannelInternals(MailCommon):
|
||||
self.assertIn('invitedMembers', res)
|
||||
self.assertEqual(res['invitedMembers'], [('ADD', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -205,7 +205,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -228,7 +228,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': last_rtc_session_id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -252,7 +252,7 @@ class TestChannelInternals(MailCommon):
|
||||
'invitedMembers': [('ADD', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -262,7 +262,7 @@ class TestChannelInternals(MailCommon):
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
@@ -281,7 +281,7 @@ class TestChannelInternals(MailCommon):
|
||||
self.assertEqual(res['invitedMembers'], [('ADD', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -291,7 +291,7 @@ class TestChannelInternals(MailCommon):
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
@@ -338,7 +338,7 @@ class TestChannelInternals(MailCommon):
|
||||
'model': 'discuss.channel',
|
||||
'invitedMembers': [('DELETE', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -358,7 +358,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id + 1,
|
||||
'channelMember': {
|
||||
"id": channel_member_test_user.id,
|
||||
"channel": {"id": channel_member_test_user.channel_id.id},
|
||||
"thread": {"id": channel_member_test_user.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member_test_user.partner_id.id,
|
||||
"name": channel_member_test_user.partner_id.name,
|
||||
@@ -405,7 +405,7 @@ class TestChannelInternals(MailCommon):
|
||||
'model': 'discuss.channel',
|
||||
'invitedMembers': [('DELETE', [{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
@@ -425,7 +425,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id + 2,
|
||||
'channelMember': {
|
||||
"id": channel_member_test_guest.id,
|
||||
"channel": {"id": channel_member_test_guest.channel_id.id},
|
||||
"thread": {"id": channel_member_test_guest.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member_test_guest.guest_id.id,
|
||||
"name": channel_member_test_guest.guest_id.name,
|
||||
@@ -481,7 +481,7 @@ class TestChannelInternals(MailCommon):
|
||||
'model': 'discuss.channel',
|
||||
'invitedMembers': [('DELETE', [{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -522,7 +522,7 @@ class TestChannelInternals(MailCommon):
|
||||
'model': 'discuss.channel',
|
||||
'invitedMembers': [('DELETE', [{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
@@ -594,7 +594,7 @@ class TestChannelInternals(MailCommon):
|
||||
'invitedMembers': [('DELETE', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -604,7 +604,7 @@ class TestChannelInternals(MailCommon):
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
@@ -668,7 +668,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -694,7 +694,7 @@ class TestChannelInternals(MailCommon):
|
||||
'id': channel_member.rtc_session_ids.id,
|
||||
'channelMember': {
|
||||
"id": channel_member.id,
|
||||
"channel": {"id": channel_member.channel_id.id},
|
||||
"thread": {"id": channel_member.channel_id.id, "model": "discuss.channel"},
|
||||
"persona": {
|
||||
"id": channel_member.partner_id.id,
|
||||
"name": channel_member.partner_id.name,
|
||||
@@ -719,7 +719,7 @@ class TestChannelInternals(MailCommon):
|
||||
'invitedMembers': [('ADD', [
|
||||
{
|
||||
'id': channel_member_test_user.id,
|
||||
'channel': {'id': channel_member_test_user.channel_id.id},
|
||||
'thread': {'id': channel_member_test_user.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_user.partner_id.id,
|
||||
'name': channel_member_test_user.partner_id.name,
|
||||
@@ -729,7 +729,7 @@ class TestChannelInternals(MailCommon):
|
||||
},
|
||||
{
|
||||
'id': channel_member_test_guest.id,
|
||||
'channel': {'id': channel_member_test_guest.channel_id.id},
|
||||
'thread': {'id': channel_member_test_guest.channel_id.id, 'model': "discuss.channel"},
|
||||
'persona': {
|
||||
'id': channel_member_test_guest.guest_id.id,
|
||||
'name': channel_member_test_guest.guest_id.name,
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestLinkPreview(MailCommon):
|
||||
'payload': {
|
||||
'LinkPreview': [{
|
||||
'id': link_preview.id,
|
||||
'message_id': message.id,
|
||||
'message': {'id': message.id},
|
||||
'image_mimetype': False,
|
||||
'og_description': self.og_description,
|
||||
'og_image': self.og_image,
|
||||
|
||||
@@ -35,12 +35,10 @@ class TestResUsersSettings(MailCommon):
|
||||
with self.assertBus(
|
||||
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||
[{
|
||||
'type': 'mail.record/insert',
|
||||
'type': 'res.users.settings',
|
||||
'payload': {
|
||||
'res.users.settings': {
|
||||
'id': settings.id,
|
||||
'is_discuss_sidebar_category_chat_open': True,
|
||||
},
|
||||
'id': settings.id,
|
||||
'is_discuss_sidebar_category_chat_open': True,
|
||||
},
|
||||
}]):
|
||||
settings.set_res_users_settings({'is_discuss_sidebar_category_chat_open': True})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1251,7 +1251,7 @@ class TestMailFormattersPerformance(BaseMailPerformance):
|
||||
|
||||
self.assertEqual(len(res), 2*2)
|
||||
for message in res:
|
||||
self.assertEqual(len(message['attachment_ids']), 2)
|
||||
self.assertEqual(len(message['attachments']), 2)
|
||||
|
||||
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
@users('employee')
|
||||
@@ -1263,7 +1263,7 @@ class TestMailFormattersPerformance(BaseMailPerformance):
|
||||
res = message.message_format()
|
||||
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(len(res[0]['attachment_ids']), 2)
|
||||
self.assertEqual(len(res[0]['attachments']), 2)
|
||||
|
||||
@mute_logger('odoo.tests', 'odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
|
||||
@users('employee')
|
||||
|
||||
@@ -13,7 +13,7 @@ export const websiteLivechatNotifications = {
|
||||
...payload,
|
||||
id: payload.id,
|
||||
model: "discuss.channel",
|
||||
type: payload.channel.channel_type,
|
||||
type: payload.channel_type,
|
||||
});
|
||||
const chatWindow = store.ChatWindow.insert({ thread: channel });
|
||||
chatWindowService.makeVisible(chatWindow);
|
||||
|
||||
Reference in New Issue
Block a user