Files
upstream-rocketchat/apps/meteor/client/lib/utils/threadMessageUtils.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

190 lines
5.5 KiB
TypeScript

import type { IMessage, IThreadMessage, MessageAttachment } from '@rocket.chat/core-typings';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import type { InfiniteData, QueryClient } from '@tanstack/react-query';
import type { Condition, Filter } from 'mongodb';
import { queryClient as defaultQueryClient } from '../queryClient';
import { roomsQueryKeys } from '../queryKeys';
export type NotifyRoomRidDeleteBulkEvent = {
rid: IMessage['rid'];
excludePinned: boolean;
ignoreDiscussion: boolean;
ts: Condition<Date>;
users: string[];
ids?: string[];
showDeletedStatus?: boolean;
} & (
| {
filesOnly: true;
replaceFileAttachmentsWith?: MessageAttachment;
}
| {
filesOnly?: false;
}
);
export const createDeleteCriteria = (params: NotifyRoomRidDeleteBulkEvent): ((message: IMessage) => boolean) => {
const query: Filter<IMessage> = {};
if (params.ids) {
query._id = { $in: params.ids };
} else {
query.ts = params.ts;
}
if (params.excludePinned) {
query.pinned = { $ne: true };
}
if (params.ignoreDiscussion) {
query.drid = { $exists: false };
}
if (params.users?.length) {
query['u.username'] = { $in: params.users };
}
return createPredicateFromFilter(query);
};
export type ThreadMessagesPage = {
items: IThreadMessage[];
itemCount: number;
};
export type ThreadMessagesInfiniteData = InfiniteData<ThreadMessagesPage, number>;
const dedupeThreadMessagesById = (messages: IThreadMessage[]): IThreadMessage[] => {
const byId = new Map<string, IThreadMessage>();
for (const message of messages) {
const existing = byId.get(message._id);
if (!existing) {
byId.set(message._id, message);
continue;
}
const messageTime = new Date(message._updatedAt ?? message.ts).getTime();
const existingTime = new Date(existing._updatedAt ?? existing.ts).getTime();
if (messageTime > existingTime) {
byId.set(message._id, message);
}
}
return Array.from(byId.values());
};
export const mutateThreadMessagesInfiniteData = (
client: QueryClient,
queryKey: readonly unknown[],
mutation: (messages: IThreadMessage[]) => void,
): void => {
client.setQueryData<ThreadMessagesInfiniteData>(queryKey, (old) => {
if (!old?.pages.length) {
return old;
}
const items = dedupeThreadMessagesById(old.pages.flatMap((page) => page.items));
const originalPageLengths = old.pages.map((page) => page.items.length);
const oldTotal = old.pages.at(-1)?.itemCount ?? 0;
const oldIds = new Set(items.map((message) => message._id));
const oldMaxTs = items.reduce((max, message) => Math.max(max, new Date(message.ts).getTime()), -Infinity);
const beforeMutationItemsLength = items.length;
mutation(items);
const afterMutationItemsLength = items.length;
const itemCountDelta = beforeMutationItemsLength - afterMutationItemsLength;
const newTotal = Math.max(0, oldTotal - itemCountDelta);
const newerInsertedCount = items.filter((message) => !oldIds.has(message._id) && new Date(message.ts).getTime() >= oldMaxTs).length;
const pages: ThreadMessagesPage[] = [];
let cursor = 0;
for (let pageIndex = 0; pageIndex < old.pages.length; pageIndex++) {
const isLastPage = pageIndex === old.pages.length - 1;
const take = isLastPage ? items.length - cursor : Math.min(originalPageLengths[pageIndex], items.length - cursor);
const slice = items.slice(cursor, cursor + Math.max(0, take));
cursor += slice.length;
pages.push({ items: slice, itemCount: newTotal });
}
return {
pages,
pageParams:
newerInsertedCount > 0
? old.pageParams.map((pageParam) => (pageParam > 0 ? pageParam + newerInsertedCount : pageParam))
: old.pageParams,
};
});
};
export const upsertThreadMessageInCache = (
message: IMessage,
rid: IMessage['rid'],
tmid: IMessage['_id'],
client: QueryClient = defaultQueryClient,
): void => {
const queryKey = roomsQueryKeys.threadMessages(rid, tmid);
if (!client.getQueryData<ThreadMessagesInfiniteData>(queryKey)) {
client.setQueryData<ThreadMessagesInfiniteData>(queryKey, {
pages: [{ items: [message as IThreadMessage], itemCount: 1 }],
pageParams: [0],
});
return;
}
mutateThreadMessagesInfiniteData(client, queryKey, (messages) => {
let shouldReturn = false;
for (let idx = 0; idx < messages.length; idx++) {
if (messages[idx]._id === message._id) {
messages[idx] = message as IThreadMessage;
shouldReturn = true;
}
}
if (shouldReturn) {
return;
}
messages.push(message as IThreadMessage);
messages.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
});
};
export const markThreadMessagesAsRead = (messages: IThreadMessage[], until?: Date): IThreadMessage[] => {
return messages.map((msg) => {
if (!msg.unread) {
return msg;
}
if (until && new Date(msg.ts) > until) {
return msg;
}
const { unread: _, ...rest } = msg;
return rest as IThreadMessage;
});
};
export const mergeThreadMessages = (cachedMessages: IThreadMessage[], fetchedMessages: IThreadMessage[]): IThreadMessage[] => {
const messageMap = new Map<string, IThreadMessage>();
for (const msg of cachedMessages) {
messageMap.set(msg._id, msg);
}
for (const msg of fetchedMessages) {
const existing = messageMap.get(msg._id);
if (!existing) {
messageMap.set(msg._id, msg);
} else {
const msgTime = new Date(msg._updatedAt ?? msg.ts).getTime();
const existingTime = new Date(existing._updatedAt ?? existing.ts).getTime();
if (msgTime > existingTime) {
messageMap.set(msg._id, msg);
}
}
}
return Array.from(messageMap.values()).sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
};