feat(messages): сообщения, реакции, пины, typing, read states и поиск
Бэкенд текстовой связи (AGENT.md 7.6, 7.15, 7.16, 8.6):
- POST/GET/PATCH/DELETE /channels/{id}/messages, ответы и упоминания,
окно правки 24 часа, права автора или MANAGE_MESSAGES;
- реакции (PUT/DELETE .../reactions/{emoji}) с агрегацией и отметкой «моя»;
- закрепления (GET/PUT/DELETE .../pins) под MANAGE_MESSAGES;
- typing с лимитом 1/3 с и адресной рассылкой всем, кроме автора;
- read states: POST /channels/{id}/ack, синхронизация READ_STATE_UPDATE между
устройствами пользователя;
- поиск FTS5 по комнате (10/мин) с экранированием запроса;
- лимиты отправки 5/5 с (burst 10) и slowmode комнаты; администратор инстанса
обходит и то, и другое (AGENT.md 7.19);
- Gateway: DispatchToChannel/DispatchToChannelExcept доставляют события комнат
только тем, кто видит комнату (VIEW_CHANNEL), права считает общий движок;
- store: messages, message_reactions, channel_read_states, files (загрузка
файлов появится вместе с вложениями).
Тесты: жизненный цикл сообщения, видимость скрытой комнаты (404 участнику,
200 админу), slowmode, упоминания и ответы, typing и read state, фильтрация
событий комнаты между двумя WS-клиентами.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchRoles, guildRolesQueryKey } from '@/api/guilds';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { SkeletonLines } from '@/components/ui/Skeleton';
|
||||
import { roleColorHex } from '@/lib/format';
|
||||
import { SettingsSection } from '@/pages/settings/SettingsSection';
|
||||
|
||||
interface GuildRolesSectionProps {
|
||||
guildId: string;
|
||||
}
|
||||
|
||||
/** «Роли»: цвет и позиция. В Фазе 1 — только чтение. */
|
||||
export function GuildRolesSection({ guildId }: GuildRolesSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const roles = useQuery({
|
||||
queryKey: guildRolesQueryKey(guildId),
|
||||
queryFn: ({ signal }) => fetchRoles(guildId, signal),
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
const sorted = (roles.data ?? []).slice().sort((left, right) => right.position - left.position);
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('settings.servers.roles.title')}
|
||||
description={t('settings.servers.roles.description')}
|
||||
>
|
||||
{roles.isPending ? (
|
||||
<SkeletonLines className="mt-4" rows={3} />
|
||||
) : roles.isError ? (
|
||||
<ErrorNotice className="mt-4" error={roles.error} onRetry={() => void roles.refetch()} />
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="mt-4 text-sm text-fg-muted">{t('settings.servers.roles.empty')}</p>
|
||||
) : (
|
||||
<ul className="gl-stagger mt-4 flex flex-col gap-1" data-testid="guild-roles">
|
||||
{sorted.map((role) => {
|
||||
const color = roleColorHex(role.color);
|
||||
return (
|
||||
<li
|
||||
key={role.id}
|
||||
className="flex items-center gap-3 rounded-[var(--radius-md)] px-2 py-1.5 transition-colors duration-[var(--duration-fast)] hover:bg-surface-2"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-full border border-border/60"
|
||||
style={color === null ? undefined : { backgroundColor: color }}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-sm">{role.name}</span>
|
||||
{role.is_default ? (
|
||||
<span className="text-xs text-fg-muted">
|
||||
{t('settings.servers.roles.default')}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-xs text-fg-muted">
|
||||
{t('settings.servers.roles.position', { position: role.position })}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user