feat(web): виды сообщений, автокомплиты композера и ссылки на комнаты

- лента оформляет виды сообщений от сервера: action (/me) — имя акцентом и
  курсив, whisper — приглушённый блок с бейджем, scream — крупнее и жирнее,
  private (/ls) — рамка, замок и бейдж «лично», system — центрированная строка
  без аватара и панели действий; реакции, ответы, правка и удаление работают
  как раньше, группировка не сливает нестандартные виды с обычными;
- автокомплиты в поле ввода с навигацией ↑/↓, Enter/Tab, Escape и мышью:
  @ — участники сервера (в DM собеседник), # — видимые текстовые комнаты,
  : — словарь на 113 эмодзи, / — команды с описанием; подсказки не появляются
  внутри слова и в блоках кода, вставка восстанавливает каретку и не расходится
  со слоем подсветки;
- `<#id>` рендерится как #имя комнаты из снапшота (иначе #id);
- 287 тестов Vitest (было 262), check/lint/test/build зелёные.
This commit is contained in:
2026-09-20 12:06:22 +03:00
parent 65d0bafe0e
commit a126f50468
22 changed files with 1826 additions and 116 deletions
+29
View File
@@ -158,4 +158,33 @@ describe('разметка: упоминания каналов', () => {
expect(container.querySelectorAll('span')).toHaveLength(0);
expect(container.textContent).toBe('см. #общий');
});
it('рендерит ссылку на комнату <#id> именем из снапшота, иначе #id', () => {
useSessionStore.setState({ guilds: [guild] });
const { container } = markup('см. <#c-1> и <#999>');
const spans = Array.from(container.querySelectorAll('span'));
expect(spans.some((span) => span.textContent === '#общий')).toBe(true);
expect(spans.some((span) => span.textContent === '#999')).toBe(true);
// Угловые скобки не остаются текстом.
expect(container.textContent).toBe('см. #общий и #999');
});
it('не ломает разметку рядом с упоминанием, ссылкой и кодом', () => {
useSessionStore.setState({ guilds: [guild] });
const { container } = render(
<MessageContent
content={'<@42> пишет в <#c-1>: https://example.com/#якорь\n`<#c-1>`'}
resolveMention={() => ({ name: 'Аля', isMe: false })}
/>,
);
expect(container.textContent).toBe('@Аля пишет в #общий: https://example.com/#якорь\n<#c-1>');
// Ссылка осталась ссылкой целиком, вместе с якорем.
expect(container.querySelector('a')).toHaveAttribute('href', 'https://example.com/#якорь');
// Внутри кода разметка не разбирается.
expect(container.querySelector('code')).toHaveTextContent('<#c-1>');
});
});