feat(web): друзья, личные беседы и вход только по приглашению
Раздел «Друзья» по запросу пользователя: - первая иконка рейки — «Друзья», серверы ниже за разделителем; личные беседы не засоряют список серверов; - сайдбар друзей: беседы с точками статуса и бейджами непрочитанного, список друзей с кликом на беседу; - контент: пустое состояние «Добавь друзей для общения» с поиском, поиск по логину (debounce), добавление в друзья, входящие и исходящие заявки, меню «…» (удалить/заблокировать), поповер последнего входа в часовом поясе пользователя (по умолчанию МСК), блок «Присоединиться по приглашению» с предпросмотром сервера; - личный чат на существующих компонентах: права по can_send канала, заголовок с именем и статусом собеседника, поиск, пины, ack и typing работают; - realtime: READY dm_channels, события DM_CHANNEL_CREATE, RELATIONSHIP_UPDATE, PRESENCE_UPDATE и MESSAGE_CREATE в беседах; - новый пользователь: «У вас пока нет серверов», создание сервера и вход по приглашению (автовступления нет); - 239 тестов Vitest (было 219), тест паритета ключей i18n (527 ключей).
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import en from '@/i18n/locales/en.json';
|
||||
import ru from '@/i18n/locales/ru.json';
|
||||
|
||||
/** Плоский список пар «ключ → значение». */
|
||||
function flatEntries(value: unknown, prefix = ''): [string, unknown][] {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return [];
|
||||
}
|
||||
const entries: [string, unknown][] = [];
|
||||
for (const [key, nested] of Object.entries(value)) {
|
||||
const path = prefix === '' ? key : `${prefix}.${key}`;
|
||||
if (typeof nested === 'object' && nested !== null) {
|
||||
entries.push(...flatEntries(nested, path));
|
||||
} else {
|
||||
entries.push([path, nested]);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Плоский список ключей вида `friends.empty.title`. */
|
||||
function flatKeys(value: unknown): string[] {
|
||||
return flatEntries(value)
|
||||
.map(([key]) => key)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Паритет переводов: русский — основной, английский должен повторять его
|
||||
* ключ в ключ, иначе часть интерфейса останется без строки.
|
||||
*/
|
||||
describe('переводы', () => {
|
||||
it('ключи ru и en совпадают', () => {
|
||||
expect(flatKeys(en)).toEqual(flatKeys(ru));
|
||||
});
|
||||
|
||||
it('нет пустых строк', () => {
|
||||
const empty = flatEntries(ru)
|
||||
.filter(([, value]) => typeof value === 'string' && value.trim() === '')
|
||||
.map(([key]) => key);
|
||||
expect(empty).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user