From ec57f8d7ff6ef804f4220c2b287393a4602e2318 Mon Sep 17 00:00:00 2001 From: zel-odoo Date: Fri, 2 Jun 2023 14:08:35 +0000 Subject: [PATCH] [IMP] mail: user searching by most recent chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task-2622887 closes odoo/odoo#122486 Signed-off-by: Alexandre Kühn (aku) --- .../static/src/composer/suggestion_service.js | 56 ++++++++++++++--- .../mail/static/src/core/persona_service.js | 24 ++++++++ .../src/discuss/core/channel_invitation.js | 8 ++- .../src/discuss_app/channel_selector.js | 20 +++--- addons/mail/static/src/web/command_palette.js | 24 +++++--- .../static/tests/discuss/suggestion_tests.js | 61 +++++++++++++++++++ 6 files changed, 166 insertions(+), 27 deletions(-) diff --git a/addons/mail/static/src/composer/suggestion_service.js b/addons/mail/static/src/composer/suggestion_service.js index 31743792df3..a96801551aa 100644 --- a/addons/mail/static/src/composer/suggestion_service.js +++ b/addons/mail/static/src/composer/suggestion_service.js @@ -142,7 +142,52 @@ export class SuggestionService { } } } - const sortFunc = (p1, p2) => { + return { + type: "Partner", + mainSuggestions: sort + ? this.sortPartnerSuggestions(mainSuggestionList, cleanedSearchTerm, thread) + : mainSuggestionList, + extraSuggestions: sort + ? this.sortPartnerSuggestions(extraSuggestionList, cleanedSearchTerm, thread) + : extraSuggestionList, + }; + } + + /** + * @param {[import("@mail/core/persona_model").Persona]} [partners] + * @param {String} [searchTerm] + * @param {import("@mail/core/thread_model").Thread} thread + * @returns {[import("@mail/core/persona_model").Persona]} + */ + sortPartnerSuggestions(partners, searchTerm = "", thread = undefined) { + const cleanedSearchTerm = cleanTerm(searchTerm); + /** + * Ordering: + * - recent chat partners + * - internal users + * - channel members + * - thread followers + * - longgest match of name, alphabetically if having same length of match + * - longgest match of email, alphabetically if having same length of match + * - id + */ + return partners.sort((p1, p2) => { + const recentChatPartnerIds = this.personaService.getRecentChatPartnerIds(); + const recentChatIndex_p1 = recentChatPartnerIds.findIndex( + (partnerId) => partnerId === p1.id + ); + const recentChatIndex_p2 = recentChatPartnerIds.findIndex( + (partnerId) => partnerId === p2.id + ); + if (recentChatIndex_p1 !== -1 && recentChatIndex_p2 === -1) { + return -1; + } else if (recentChatIndex_p1 === -1 && recentChatIndex_p2 !== -1) { + return 1; + } else if (recentChatIndex_p1 < recentChatIndex_p2) { + return -1; + } else if (recentChatIndex_p1 > recentChatIndex_p2) { + return 1; + } const isAInternalUser = p1.user?.isInternalUser; const isBInternalUser = p2.user?.isInternalUser; if (isAInternalUser && !isBInternalUser) { @@ -151,7 +196,7 @@ export class SuggestionService { if (!isAInternalUser && isBInternalUser) { return 1; } - if (thread.model === "discuss.channel") { + if (thread?.model === "discuss.channel") { const isMember1 = thread.channelMembers.some((member) => member.persona === p1); const isMember2 = thread.channelMembers.some((member) => member.persona === p2); if (isMember1 && !isMember2) { @@ -212,12 +257,7 @@ export class SuggestionService { return 1; } return p1.id - p2.id; - }; - return { - type: "Partner", - mainSuggestions: sort ? mainSuggestionList.sort(sortFunc) : mainSuggestionList, - extraSuggestions: sort ? extraSuggestionList.sort(sortFunc) : extraSuggestionList, - }; + }); } searchChannelSuggestions(cleanedSearchTerm, thread, sort) { diff --git a/addons/mail/static/src/core/persona_service.js b/addons/mail/static/src/core/persona_service.js index 95e2cdaba57..77b78d8ef6b 100644 --- a/addons/mail/static/src/core/persona_service.js +++ b/addons/mail/static/src/core/persona_service.js @@ -55,6 +55,30 @@ export class PersonaService { this.store.registeredImStatusPartners?.push(persona.id); } } + + /** + * List of known partner ids with a direct chat, ordered + * by most recent interest (1st item being the most recent) + * + * @returns {[integer]} + */ + getRecentChatPartnerIds() { + return Object.values(this.store.threads) + .filter((thread) => thread.type === "chat") + .sort((a, b) => { + if (!a.lastInterestDateTime && !b.lastInterestDateTime) { + return 0; + } + if (a.lastInterestDateTime && !b.lastInterestDateTime) { + return -1; + } + if (!a.lastInterestDateTime && b.lastInterestDateTime) { + return 1; + } + return b.lastInterestDateTime.ts - a.lastInterestDateTime.ts; + }) + .map((thread) => thread.chatPartnerId); + } } export const personaService = { diff --git a/addons/mail/static/src/discuss/core/channel_invitation.js b/addons/mail/static/src/discuss/core/channel_invitation.js index 8d3ff94b896..510fbda3597 100644 --- a/addons/mail/static/src/discuss/core/channel_invitation.js +++ b/addons/mail/static/src/discuss/core/channel_invitation.js @@ -19,6 +19,8 @@ export class ChannelInvitation extends Component { this.notification = useService("notification"); this.threadService = useState(useService("mail.thread")); this.personaService = useService("mail.persona"); + /** @type {import("@mail/composer/suggestion_service").SuggestionService} */ + this.suggestionService = useService("mail.suggestion"); this.ui = useService("ui"); this.inputRef = useRef("input"); this.searchStr = ""; @@ -56,7 +58,11 @@ export class ChannelInvitation extends Component { }); selectablePartners.push(newPartner); } - this.state.selectablePartners = selectablePartners; + this.state.selectablePartners = this.suggestionService.sortPartnerSuggestions( + selectablePartners, + this.searchStr, + this.props.thread + ); this.state.searchResultCount = results["count"]; } diff --git a/addons/mail/static/src/discuss_app/channel_selector.js b/addons/mail/static/src/discuss_app/channel_selector.js index e8f93ff38c9..f9362e19593 100644 --- a/addons/mail/static/src/discuss_app/channel_selector.js +++ b/addons/mail/static/src/discuss_app/channel_selector.js @@ -20,6 +20,8 @@ export class ChannelSelector extends Component { this.store = useStore(); this.threadService = useState(useService("mail.thread")); this.personaService = useService("mail.persona"); + /** @type {import("@mail/composer/suggestion_service").SuggestionService} */ + this.suggestionService = useService("mail.suggestion"); this.orm = useService("orm"); this.state = useState({ value: "", @@ -65,14 +67,16 @@ export class ChannelSelector extends Component { 10, this.state.selectedPartners, ]); - const suggestions = results.map((data) => { - this.personaService.insert({ ...data, type: "partner" }); - return { - classList: "o-mail-ChannelSelector-suggestion", - label: data.name, - partner: data, - }; - }); + const suggestions = this.suggestionService + .sortPartnerSuggestions(results, cleanedTerm) + .map((data) => { + this.personaService.insert({ ...data, type: "partner" }); + return { + classList: "o-mail-ChannelSelector-suggestion", + label: data.name, + partner: data, + }; + }); if (this.store.self.name.includes(cleanedTerm)) { suggestions.push({ classList: "o-mail-ChannelSelector-suggestion", diff --git a/addons/mail/static/src/web/command_palette.js b/addons/mail/static/src/web/command_palette.js index 96b0af0a554..4f0137b981f 100644 --- a/addons/mail/static/src/web/command_palette.js +++ b/addons/mail/static/src/web/command_palette.js @@ -32,17 +32,21 @@ commandProviderRegistry.add("mail.partner", { /** @type {import("@mail/core/messaging_service").Messaging} */ const messaging = env.services["mail.messaging"]; const threadService = env.services["mail.thread"]; + /** @type {import("@mail/composer/suggestion_service").SuggestionService} */ + const suggestionService = env.services["mail.suggestion"]; const results = await messaging.searchPartners(options.searchValue); - return results.map(function (partner) { - return { - Component: DialogCommand, - action() { - threadService.openChat({ partnerId: partner.id }); - }, - name: partner.name, - props: { email: partner.email }, - }; - }); + return suggestionService + .sortPartnerSuggestions(results, options.searchValue) + .map(function (partner) { + return { + Component: DialogCommand, + action() { + threadService.openChat({ partnerId: partner.id }); + }, + name: partner.name, + props: { email: partner.email }, + }; + }); }, }); diff --git a/addons/mail/static/tests/discuss/suggestion_tests.js b/addons/mail/static/tests/discuss/suggestion_tests.js index f6f7d6cb244..1b24ef1cc34 100644 --- a/addons/mail/static/tests/discuss/suggestion_tests.js +++ b/addons/mail/static/tests/discuss/suggestion_tests.js @@ -2,6 +2,7 @@ import { Composer } from "@mail/composer/composer"; import { click, insertText, start, startServer } from "@mail/../tests/helpers/test_utils"; +import { Command } from "@mail/../tests/helpers/command"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; QUnit.module("suggestion", { @@ -62,3 +63,63 @@ QUnit.test( assert.containsNone($, ".o-mail-Composer-suggestionList .o-open"); } ); + +QUnit.test("Sort partner suggestions by recent chats", async (assert) => { + const pyEnv = await startServer(); + const [partner_1, partner_2, partner_3] = pyEnv["res.partner"].create([ + { name: "User 1" }, + { name: "User 2" }, + { name: "User 3" }, + ]); + pyEnv["res.users"].create([ + { partner_id: partner_1 }, + { partner_id: partner_2 }, + { partner_id: partner_3 }, + ]); + pyEnv["discuss.channel"].create([ + { name: "General", channel_type: "channel" }, + { + channel_member_ids: [ + Command.create({ + last_interest_dt: "2023-01-01 00:00:00", + partner_id: pyEnv.currentPartnerId, + }), + Command.create({ partner_id: partner_1 }), + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + Command.create({ + last_interest_dt: "2023-01-01 00:00:00", + partner_id: pyEnv.currentPartnerId, + }), + Command.create({ partner_id: partner_2 }), + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + Command.create({ + last_interest_dt: "2023-01-01 00:00:00", + partner_id: pyEnv.currentPartnerId, + }), + Command.create({ partner_id: partner_3 }), + ], + channel_type: "chat", + }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + await click(".o-mail-DiscussCategoryItem:contains('User 2')"); + await insertText(".o-mail-Composer-input", "This is a test"); + await click(".o-mail-Composer-send:not(:disabled)"); + assert.containsOnce($, ".o-mail-Message:contains('This is a test')"); + await click(".o-mail-DiscussCategoryItem:contains('General')"); + await insertText(".o-mail-Composer-input", "@"); + await insertText(".o-mail-Composer-input", "User"); + assert.containsN($, ".o-mail-Composer-suggestion", 3); + assert.strictEqual($(".o-mail-Composer-suggestion").eq(0).text(), "User 2"); + assert.strictEqual($(".o-mail-Composer-suggestion").eq(1).text(), "User 1"); + assert.strictEqual($(".o-mail-Composer-suggestion").eq(2).text(), "User 3"); +});