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
190 lines
5.5 KiB
TypeScript
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());
|
|
};
|