diff --git a/mail_gateway/models/discuss_channel.py b/mail_gateway/models/discuss_channel.py index 5d1381058..e1fc91306 100644 --- a/mail_gateway/models/discuss_channel.py +++ b/mail_gateway/models/discuss_channel.py @@ -30,19 +30,74 @@ class DiscussChannel(models.Model): def _to_store(self, store: Store, fields): res = super()._to_store(store, fields) - for record in self.filtered("gateway_id"): - if not record: - continue + gw_channels = self.filtered("gateway_id") + if not gw_channels: + return res + # sudo: res.partner.gateway.channel - technical lookup by token, done + # once for the whole recordset (this runs for every channel on load) + partner_by_key = {} + for gw_channel in ( + self.env["res.partner.gateway.channel"] + .sudo() + .search( + [ + ("gateway_id", "in", gw_channels.gateway_id.ids), + ( + "gateway_token", + "in", + [t for t in gw_channels.mapped("gateway_channel_token") if t], + ), + ] + ) + ): + partner_by_key.setdefault( + (gw_channel.gateway_id.id, gw_channel.gateway_token), + gw_channel.partner_id, + ) + for record in gw_channels: + guests = record.channel_member_ids.guest_id + partner = partner_by_key.get( + (record.gateway_id.id, record.gateway_channel_token), + self.env["res.partner"], + ) store.add_model_values( self._name, { "id": record.id, "gateway": record.gateway_id._gateway_info(), "gateway_id": record.id, + "gateway_partner_id": partner.id, + "gateway_guest_id": guests.id if len(guests) == 1 else False, }, ) return res + def _get_gateway_partner(self): + """Partner linked to the gateway token of the channel, if any.""" + self.ensure_one() + if not self.gateway_id or not self.gateway_channel_token: + return self.env["res.partner"] + # sudo: res.partner.gateway.channel - technical lookup by token + return ( + self.env["res.partner.gateway.channel"] + .sudo() + .search( + [ + ("gateway_id", "=", self.gateway_id.id), + ("gateway_token", "=", self.gateway_channel_token), + ], + limit=1, + ) + .partner_id + ) + + def _get_gateway_guest(self): + """Guest of the channel when there is exactly one (sender not yet + linked to a partner), used by the "New Partner" action.""" + self.ensure_one() + guests = self.channel_member_ids.guest_id + return guests if len(guests) == 1 else self.env["mail.guest"] + def _generate_avatar_gateway(self): # We will use this function to set a default avatar on each module return False diff --git a/mail_gateway/static/src/core/common/message_model_patch.esm.js b/mail_gateway/static/src/core/common/message_model_patch.esm.js index 1866de703..18ea60a09 100644 --- a/mail_gateway/static/src/core/common/message_model_patch.esm.js +++ b/mail_gateway/static/src/core/common/message_model_patch.esm.js @@ -1,6 +1,6 @@ import {Message} from "@mail/core/common/message_model"; import {patch} from "@web/core/utils/patch"; -import {stateToUrl} from "@web/core/browser/router"; +import {router} from "@web/core/browser/router"; import {url} from "@web/core/utils/urls"; patch(Message.prototype, { @@ -13,7 +13,7 @@ patch(Message.prototype, { return super.resUrl; } return url( - stateToUrl({ + router.stateToUrl({ model: this.gateway_thread_data.model, resId: this.gateway_thread_data.id, }) diff --git a/mail_gateway/static/src/core/common/thread_actions.esm.js b/mail_gateway/static/src/core/common/thread_actions.esm.js index 48c43b74c..3152a9c84 100644 --- a/mail_gateway/static/src/core/common/thread_actions.esm.js +++ b/mail_gateway/static/src/core/common/thread_actions.esm.js @@ -1,97 +1,65 @@ import {_t} from "@web/core/l10n/translation"; import {threadActionsRegistry} from "@mail/core/common/thread_actions"; +/* + * Conditions are evaluated on every render: they must stay synchronous and free + * of side effects. Starting an RPC and re-rendering the owner from here keeps + * the Discuss fiber from completing (no messages are shown). The data they need + * (gateway partner and guest) comes with the channel store data. + */ +function isVisibleChannel(owner, thread) { + return ( + thread?.model === "discuss.channel" && + (!owner.props.chatWindow || owner.props.chatWindow.isOpen) + ); +} + +function getProfilePartnerId(thread) { + if (thread.channel_type === "gateway") { + return thread.gateway_partner_id || false; + } + return thread.correspondent?.partner_id?.id || false; +} + threadActionsRegistry .add("open-gw-new-partner", { - condition({owner, thread}) { - if ( - thread?.model !== "discuss.channel" || - (owner.props.chatWindow && !owner.props.chatWindow.isOpen) - ) { - return false; - } - if (thread._guestChecked) { - return Boolean(thread._cachedGuestId); - } - const orm = owner.env.services.orm; - (async () => { - const members = await orm.silent.searchRead( - "discuss.channel.member", - [["channel_id", "=", thread.id]], - ["guest_id"] - ); - const guestMembers = members.filter((m) => m.guest_id); - if (guestMembers.length === 1) { - thread._cachedGuestId = guestMembers[0].guest_id; - } else { - thread._cachedGuestId = null; - } - thread._guestChecked = true; - owner.render?.(); - })(); - return false; - }, + condition: ({owner, thread}) => + isVisibleChannel(owner, thread) && Boolean(thread.gateway_guest_id), icon: "fa fa-fw fa-address-book", name: _t("New Partner"), async open({owner, thread}) { - const guestId = thread._cachedGuestId; - if (!guestId) return; - await owner.env.services.action.doAction({ - type: "ir.actions.act_window", - res_model: "mail.guest.manage", - context: {default_guest_id: guestId[0]}, - views: [[false, "form"]], - target: "new", - }); + const guestId = thread.gateway_guest_id; + if (!guestId) { + return; + } + await owner.env.services.action.doAction( + { + type: "ir.actions.act_window", + res_model: "mail.guest.manage", + context: {default_guest_id: guestId}, + views: [[false, "form"]], + target: "new", + }, + { + // Refresh the gateway partner/guest once the guest is managed + onClose: () => + thread.store.fetchStoreData("discuss.channel", [thread.id]), + } + ); }, iconLarge: "fa fa-fw fa-lg fa-address-book", sequence: 18, }) .add("open-gw-profile", { - condition({owner, thread}) { - if ( - thread?.model !== "discuss.channel" || - (owner.props.chatWindow && !owner.props.chatWindow.isOpen) - ) { - return false; - } - if (thread._partnerIdChecked) { - return Boolean(thread._cachedPartnerId); - } - const orm = owner.env.services.orm; - (async () => { - const channelData = await orm.silent.searchRead( - "discuss.channel", - [["id", "=", thread.id]], - ["gateway_channel_token"] - ); - if (channelData[0]?.gateway_channel_token) { - const gatewayChannel = await orm.silent.searchRead( - "res.partner.gateway.channel", - [["gateway_token", "=", channelData[0].gateway_channel_token]], - ["partner_id"] - ); - thread._cachedPartnerId = Array.isArray( - gatewayChannel[0]?.partner_id - ) - ? gatewayChannel[0].partner_id[0] - : gatewayChannel[0]?.partner_id; - } else { - thread._cachedPartnerId = thread.correspondent?.persona?.id; - } - thread._partnerIdChecked = true; - owner.render?.(); - })(); - return false; - }, + condition: ({owner, thread}) => + isVisibleChannel(owner, thread) && Boolean(getProfilePartnerId(thread)), icon: "fa fa-fw fa-user-circle-o", name: _t("Open Contact"), async open({owner, thread}) { - const partnerId = thread._cachedPartnerId; await owner.env.services.action.doAction({ type: "ir.actions.act_window", res_model: "res.partner", - res_id: partnerId, + res_id: getProfilePartnerId(thread), views: [[false, "form"]], }); }, diff --git a/mail_gateway/static/src/core/common/thread_model_patch.esm.js b/mail_gateway/static/src/core/common/thread_model_patch.esm.js index 8787b07cb..2c4049fdf 100644 --- a/mail_gateway/static/src/core/common/thread_model_patch.esm.js +++ b/mail_gateway/static/src/core/common/thread_model_patch.esm.js @@ -8,7 +8,12 @@ patch(Thread, { _insert(data) { const thread = super._insert(...arguments); if (thread.channel_type === "gateway") { - assignIn(thread, data, ["anonymous_name", "gateway"]); + assignIn(thread, data, [ + "anonymous_name", + "gateway", + "gateway_partner_id", + "gateway_guest_id", + ]); } return thread; }, diff --git a/mail_gateway/tests/test_mail_gateway.py b/mail_gateway/tests/test_mail_gateway.py index a54fe8782..0e4e457bd 100644 --- a/mail_gateway/tests/test_mail_gateway.py +++ b/mail_gateway/tests/test_mail_gateway.py @@ -480,6 +480,39 @@ def test_discuss_channel_to_store(self): ["gateway_id", "gateway_token", "gateway_message_ids", "channel_type"], ) + def test_discuss_channel_to_store_gateway_partner(self): + impl = self.env["mail.gateway.abstract"] + channel = impl._get_channel( + self.gateway, "ch-partner-tok", {}, force_create=True + ) + self.assertFalse(channel._get_gateway_partner()) + self.env["res.partner.gateway.channel"].create( + { + "partner_id": self.partner.id, + "gateway_id": self.gateway.id, + "gateway_token": "ch-partner-tok", + } + ) + self.assertEqual(channel._get_gateway_partner(), self.partner) + store = Store() + channel._to_store(store, ["channel_type"]) + values = [ + value + for value in store.get_result()["discuss.channel"] + if value["id"] == channel.id + ] + self.assertTrue( + any(value.get("gateway_partner_id") == self.partner.id for value in values) + ) + + def test_get_gateway_guest(self): + impl = self.env["mail.gateway.abstract"] + channel = impl._get_channel(self.gateway, "ch-guest-tok", {}, force_create=True) + self.assertFalse(channel._get_gateway_guest()) + guest = self.env["mail.guest"].create({"name": "GW Guest"}) + channel._add_members(guests=guest) + self.assertEqual(channel._get_gateway_guest(), guest) + # ── mail.thread additional coverage ─────────────────────────────────────── def test_get_notify_valid_parameters(self):