import type { ReactNode } from 'react'; import type { Person } from './types'; /** Room-wide mention tokens, always offered alongside members. */ export const SPECIAL_MENTIONS = ['channel', 'here']; function esc(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** The @token currently being typed at the end of the draft (caret-at-end), or null. */ export function trailingMentionQuery(draft: string): string | null { const m = draft.match(/(?:^|\s)@([\w]*)$/); return m ? (m[1] ?? '') : null; } /** Replace the trailing @query with `@insert ` (keeping the leading boundary). */ export function insertMention(draft: string, insert: string): string { return draft.replace(/(^|\s)@([\w]*)$/, (_full, lead: string) => `${lead}@${insert} `); } /** Resolve the opaque mention userId list from the final text + known members. */ export function resolveMentions(text: string, members: Person[]): string[] { const ids = new Set(); for (const p of members) if (text.includes(`@${p.name}`)) ids.add(p.id); if (/@channel\b/.test(text) || /@here\b/.test(text)) for (const p of members) ids.add(p.id); return [...ids]; } /** Render text with @mentions (member names + @channel/@here) wrapped for highlighting. */ export function highlightMentions(text: string, memberNames: string[]): ReactNode[] { // Longest-first so "@Sofia Ramirez" wins over a bare "@Sofia". const names = [...new Set([...memberNames, ...SPECIAL_MENTIONS])].filter(Boolean).sort((a, b) => b.length - a.length); if (names.length === 0) return [text]; const re = new RegExp(`@(${names.map(esc).join('|')})`, 'g'); const out: ReactNode[] = []; let last = 0; let key = 0; let m: RegExpExecArray | null; while ((m = re.exec(text)) !== null) { if (m.index > last) out.push(text.slice(last, m.index)); out.push( {m[0]} , ); last = m.index + m[0].length; } if (last < text.length) out.push(text.slice(last)); return out.length > 0 ? out : [text]; }