104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
|
|
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.";
|
||
|
|
}
|