feat: remove CommunicationsQuery, load messages on-demand only

- Remove bulk CommunicationsQuery from useContacts (was loading ALL
  messages for ALL contacts on init)
- Rebuild commThreads from contacts + contactInboxes using the new
  lastMessageText field from Phase 1
- Per-contact messages now load on-demand via getClientTimeline
- Remove commItems from useWorkspaceRouting, use clientTimelineItems

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-24 20:02:58 +07:00
parent 601de37ab0
commit ac9c50b47d
3 changed files with 28 additions and 75 deletions

View File

@@ -95,7 +95,6 @@ const {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const { const {
contacts, contacts,
commItems,
contactSearch, contactSearch,
selectedChannel, selectedChannel,
sortMode, sortMode,
@@ -110,7 +109,6 @@ const {
markAvatarBroken, markAvatarBroken,
contactInitials, contactInitials,
refetchContacts, refetchContacts,
refetchCommunications,
} = useContacts({ apolloAuthReady }); } = useContacts({ apolloAuthReady });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -301,7 +299,6 @@ const {
async function refetchAllCrmQueries() { async function refetchAllCrmQueries() {
await Promise.all([ await Promise.all([
refetchContacts(), refetchContacts(),
refetchCommunications(),
refetchContactInboxes(), refetchContactInboxes(),
refetchCalendar(), refetchCalendar(),
refetchDeals(), refetchDeals(),
@@ -461,59 +458,36 @@ const pilotHeaderPhrases = [
const pilotHeaderText = ref("Every step moves you forward"); const pilotHeaderText = ref("Every step moves you forward");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Comm Threads (glue: contacts + commItems + contactInboxes) // Comm Threads (glue: contacts + contactInboxes — no bulk message loading)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const commThreads = computed(() => { const commThreads = computed(() => {
const sorted = [...commItems.value].sort((a, b) => a.at.localeCompare(b.at));
const map = new Map<string, CommItem[]>();
for (const item of sorted) {
if (!map.has(item.contact)) map.set(item.contact, []);
map.get(item.contact)?.push(item);
}
const contactById = new Map(contacts.value.map((contact) => [contact.id, contact]));
const inboxesByContactId = new Map<string, ContactInbox[]>(); const inboxesByContactId = new Map<string, ContactInbox[]>();
for (const inbox of contactInboxes.value) { for (const inbox of contactInboxes.value) {
if (!inboxesByContactId.has(inbox.contactId)) inboxesByContactId.set(inbox.contactId, []); if (!inboxesByContactId.has(inbox.contactId)) inboxesByContactId.set(inbox.contactId, []);
inboxesByContactId.get(inbox.contactId)?.push(inbox); inboxesByContactId.get(inbox.contactId)!.push(inbox);
} }
const contactIds = new Set<string>([ return contacts.value
...contacts.value.map((contact) => contact.id), .map((c) => {
...contactInboxes.value.map((inbox) => inbox.contactId), const inboxes = inboxesByContactId.get(c.id) ?? [];
]);
return [...contactIds]
.map((contactId) => {
const contact = contactById.get(contactId);
const inboxes = inboxesByContactId.get(contactId) ?? [];
const contactName = contact?.name ?? inboxes[0]?.contactName ?? "";
const items = map.get(contactName) ?? [];
const last = items[items.length - 1];
const channels = [ const channels = [
...new Set([ ...new Set([
...(contact?.channels ?? []), ...c.channels,
...inboxes.map((inbox) => inbox.channel), ...inboxes.map((i) => i.channel),
...items.map((item) => item.channel),
]), ]),
] as CommItem["channel"][]; ] as CommItem["channel"][];
const inboxFallbackLast = inboxes
.map((inbox) => inbox.lastMessageAt || inbox.updatedAt)
.filter(Boolean)
.sort()
.at(-1);
return { return {
id: contactId, id: c.id,
contact: contactName, contact: c.name,
avatar: contact?.avatar ?? "", avatar: c.avatar,
channels, channels,
lastAt: last?.at ?? contact?.lastContactAt ?? inboxFallbackLast ?? "", lastAt: c.lastContactAt,
lastText: last?.text ?? "No messages yet", lastText: c.lastMessageText || "No messages yet",
items, items: [] as CommItem[],
}; };
}) })
.filter((thread) => thread.contact) .filter((t) => t.contact)
.sort((a, b) => b.lastAt.localeCompare(a.lastAt)); .sort((a, b) => b.lastAt.localeCompare(a.lastAt));
}); });
@@ -722,7 +696,7 @@ const routing = useWorkspaceRouting({
commThreads, commThreads,
contacts, contacts,
deals, deals,
commItems, clientTimelineItems,
activeChangeMessage, activeChangeMessage,
activeChangeItem, activeChangeItem,
activeChangeItems, activeChangeItems,
@@ -958,8 +932,9 @@ const selectedContactEvents = computed(() => {
const selectedContactRecentMessages = computed(() => { const selectedContactRecentMessages = computed(() => {
if (!selectedContact.value) return []; if (!selectedContact.value) return [];
return commItems.value return clientTimelineItems.value
.filter((item) => item.contact === selectedContact.value?.name && item.kind === "message") .filter((entry) => entry.contentType === "message" && entry.message)
.map((entry) => entry.message!)
.sort((a, b) => b.at.localeCompare(a.at)) .sort((a, b) => b.at.localeCompare(a.at))
.slice(0, 8); .slice(0, 8);
}); });

View File

@@ -1,9 +1,6 @@
import { ref, computed, watch, watchEffect, type ComputedRef } from "vue"; import { ref, computed, watch, watchEffect, type ComputedRef } from "vue";
import { useQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import { import { ContactsQueryDocument } from "~~/graphql/generated";
ContactsQueryDocument,
CommunicationsQueryDocument,
} from "~~/graphql/generated";
export type Contact = { export type Contact = {
id: string; id: string;
@@ -11,6 +8,8 @@ export type Contact = {
avatar: string; avatar: string;
channels: string[]; channels: string[];
lastContactAt: string; lastContactAt: string;
lastMessageText: string;
lastMessageChannel: string;
description: string; description: string;
}; };
@@ -41,32 +40,13 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef<boolean> }) {
{ enabled: opts.apolloAuthReady }, { enabled: opts.apolloAuthReady },
); );
const { result: communicationsResult, refetch: refetchCommunications } = useQuery(
CommunicationsQueryDocument,
null,
{ enabled: opts.apolloAuthReady },
);
const contacts = ref<Contact[]>([]); const contacts = ref<Contact[]>([]);
const commItems = ref<CommItem[]>([]);
watch( watch(
[() => contactsResult.value?.contacts, () => communicationsResult.value?.communications], () => contactsResult.value?.contacts,
([rawContacts, rawComms]) => { (rawContacts) => {
if (!rawContacts) return; if (!rawContacts) return;
const contactsList = [...rawContacts] as Contact[]; contacts.value = [...rawContacts] as Contact[];
const commsList = (rawComms ?? []) as CommItem[];
const byName = new Map<string, Set<string>>();
for (const item of commsList) {
if (!byName.has(item.contact)) byName.set(item.contact, new Set());
byName.get(item.contact)?.add(item.channel);
}
contacts.value = contactsList.map((c) => ({
...c,
channels: Array.from(byName.get(c.name) ?? c.channels ?? []),
}));
commItems.value = commsList;
}, },
{ immediate: true }, { immediate: true },
); );
@@ -166,7 +146,6 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef<boolean> }) {
return { return {
contacts, contacts,
commItems,
contactSearch, contactSearch,
selectedChannel, selectedChannel,
sortMode, sortMode,
@@ -181,6 +160,5 @@ export function useContacts(opts: { apolloAuthReady: ComputedRef<boolean> }) {
markAvatarBroken, markAvatarBroken,
contactInitials, contactInitials,
refetchContacts, refetchContacts,
refetchCommunications,
}; };
} }

View File

@@ -31,7 +31,7 @@ export function useWorkspaceRouting(opts: {
commThreads: ComputedRef<{ id: string; [key: string]: any }[]>; commThreads: ComputedRef<{ id: string; [key: string]: any }[]>;
contacts: Ref<{ id: string; name: string; [key: string]: any }[]>; contacts: Ref<{ id: string; name: string; [key: string]: any }[]>;
deals: Ref<{ id: string; contact: string; [key: string]: any }[]>; deals: Ref<{ id: string; contact: string; [key: string]: any }[]>;
commItems: Ref<{ id: string; contact: string; [key: string]: any }[]>; clientTimelineItems: Ref<{ id: string; contactId: string; contentType: string; message?: { contact: string } | null; [key: string]: any }[]>;
activeChangeMessage: ComputedRef<{ changeSetId?: string | null; changeItems?: PilotChangeItem[] | null } | null>; activeChangeMessage: ComputedRef<{ changeSetId?: string | null; changeItems?: PilotChangeItem[] | null } | null>;
activeChangeItem: ComputedRef<PilotChangeItem | null>; activeChangeItem: ComputedRef<PilotChangeItem | null>;
activeChangeItems: ComputedRef<PilotChangeItem[]>; activeChangeItems: ComputedRef<PilotChangeItem[]>;
@@ -340,9 +340,9 @@ export function useWorkspaceRouting(opts: {
if (item.entity === "message" && item.entityId) { if (item.entity === "message" && item.entityId) {
opts.peopleLeftMode.value = "contacts"; opts.peopleLeftMode.value = "contacts";
opts.peopleListMode.value = "contacts"; opts.peopleListMode.value = "contacts";
const message = opts.commItems.value.find((entry) => entry.id === item.entityId); const timelineEntry = opts.clientTimelineItems.value.find((entry) => entry.contentType === "message" && entry.message && entry.id === item.entityId);
if (message?.contact) { if (timelineEntry?.message?.contact) {
opts.openCommunicationThread(message.contact); opts.openCommunicationThread(timelineEntry.message.contact);
} }
opts.focusedCalendarEventId.value = ""; opts.focusedCalendarEventId.value = "";
syncPathFromUi(push); syncPathFromUi(push);