lazyBoy/apps/web/src/mentions.ts

57 lines
2.0 KiB
TypeScript

import type { RoomMember } from "./types";
/** One row of the `@` list. A null `member` is the whole-room escape hatch. */
export interface MentionChoice {
name: string;
member: RoomMember | null;
}
/**
* The `@`-token under the caret, or null when the caret is not writing one.
* A token ends at a space, so `@小美 幫我看` stops offering the list the moment
* the name is finished, and `mail@example.com` never opens it.
*/
export function mentionToken(text: string, caret: number): string | null {
const end = Math.max(0, Math.min(caret, text.length));
const hit = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, end));
return hit ? hit[1] : null;
}
/**
* Who the token could mean, in room order, capped so the list stays one glance.
* `everyone` — the one name that reaches the whole room — leads the list while
* the token is empty or could still become it, because typing `@` is where a
* person discovers that escape hatch.
*/
export function mentionChoices(
token: string,
members: RoomMember[],
everyone: string,
): MentionChoice[] {
const needle = token.trim().toLowerCase();
const choices: MentionChoice[] = [];
if (everyone.toLowerCase().startsWith(needle)) {
choices.push({ name: everyone, member: null });
}
for (const member of members) {
if (member.name.toLowerCase().includes(needle)) choices.push({ name: member.name, member });
}
return choices.slice(0, 6);
}
/**
* Replace the token being typed with a chosen name and one space, leaving the
* rest of the message where it was. The new caret sits after that space, so the
* next keystroke continues the sentence instead of reopening the list.
*/
export function acceptMention(
text: string,
caret: number,
name: string,
): { text: string; caret: number } {
const end = Math.max(0, Math.min(caret, text.length));
const head = text.slice(0, end).replace(/@([^\s@]*)$/, "");
const inserted = `${head}@${name} `;
return { text: inserted + text.slice(end), caret: inserted.length };
}