Submatter/apps/web/components/chat/chat-utils.ts

104 lines
3.1 KiB
TypeScript
Raw Normal View History

export function mergeChatMessages<
T extends { id: string; sequence: string; revision?: number },
>(current: T[], incoming: T[]): T[] {
const byId = new Map(current.map((message) => [message.id, message]));
for (const message of incoming) {
const previous = byId.get(message.id);
if (!previous || (message.revision ?? 1) >= (previous.revision ?? 1))
byId.set(message.id, message);
}
return [...byId.values()].sort((a, b) => {
const left = BigInt(a.sequence);
const right = BigInt(b.sequence);
return left < right ? -1 : left > right ? 1 : 0;
});
}
// Each async continuation retains the generation it started in. A return to the
// same channel is still a new generation, so old completions cannot re-enter it.
export function createChatScope(workspace: string, channelId?: string) {
let key = JSON.stringify([workspace, channelId ?? ""]);
let generation = 0;
let revoked = false;
return {
select(nextWorkspace: string, nextChannelId?: string) {
const next = JSON.stringify([nextWorkspace, nextChannelId ?? ""]);
if (next === key) return;
key = next;
generation++;
revoked = false;
},
revoke() {
if (!revoked) {
revoked = true;
generation++;
}
},
dispose() {
revoked = true;
generation++;
},
capture() {
const captured = generation;
return () => !revoked && captured === generation;
},
captureCleanup() {
const captured = generation;
return () => revoked && captured === generation;
},
isRevoked() {
return revoked;
},
};
}
export type CatchUpWindow = { after: string; through?: string };
// Keep the first response's high-water fixed until every page of that interval is read.
export function advanceCatchUpWindow(
window: CatchUpWindow,
page: { next: string; through: string; hasMore: boolean },
): CatchUpWindow {
if (page.hasMore) {
if (BigInt(page.next) <= BigInt(window.after)) return window;
return { after: page.next, through: window.through ?? page.through };
}
return { after: page.through };
}
export function rootReadIsProven(
sequence: string,
scanned: string | null,
unseenReply: string | null,
readBoundary: string,
): boolean {
return (
scanned !== null &&
BigInt(scanned) >= BigInt(sequence) &&
(unseenReply === null ||
BigInt(unseenReply) > BigInt(sequence) ||
BigInt(unseenReply) <= BigInt(readBoundary))
);
}
export function chatBadge(summary: {
unreadCount: number;
mentionCount: number;
directUnreadCount: number;
}) {
return {
dot: summary.unreadCount > 0,
count: summary.mentionCount + summary.directUnreadCount,
};
}
export function messageError(code?: string): string {
if (code === "NOT_FOUND" || code === "FORBIDDEN")
return "You no longer have access to this channel.";
if (code === "CONFLICT")
return "This request was already used for a different message. Please compose a new message.";
if (code === "BAD_REQUEST")
return "This message could not be sent. Check the text and retry.";
return "Could not send message. Please retry.";
}