feat(profile): системные бейджи рядом с ником

- участники сервера отдают список бейджей из badges_json (AGENT.md 7.2):
  список расширяемый, назначается только системой
- клиент: каталог бейджей (щит администратора инстанса, корона владельца,
  ранний сторонник, проверенный), неизвестные показываются нейтральной меткой
- бейджи видны в ленте, списке участников и друзьях вместо текстовой плашки
- тесты: Go (бейджи в списке участников) и Vitest (каталог и отрисовка)
This commit is contained in:
2026-09-21 18:10:39 +03:00
parent 0f0060e8a4
commit b43452aa04
19 changed files with 266 additions and 19 deletions
+16 -5
View File
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import type { GuildMember } from '@/api/types';
import { FramedAvatar, Nickname } from '@/components/profile/Cosmetics';
import { SystemBadges } from '@/components/profile/SystemBadges';
import { ErrorNotice } from '@/components/ui/ErrorNotice';
import { Badge } from '@/components/ui/primitives';
import { SkeletonLines } from '@/components/ui/Skeleton';
@@ -20,6 +21,8 @@ interface GuildMembersSectionProps {
canTimeout?: boolean;
/** Свой профиль: себя модерировать нельзя. */
currentUserId?: string | undefined;
/** Владелец сервера: рядом с ником показывается его бейдж (AGENT.md 7.2). */
ownerId?: string | undefined;
}
/** Сколько участников показываем в превью (полный список — в следующих фазах). */
@@ -36,6 +39,7 @@ export function GuildMembersSection({
canBan = false,
canTimeout = false,
currentUserId,
ownerId,
}: GuildMembersSectionProps) {
const { t } = useTranslation();
const preview = (members ?? []).slice(0, PREVIEW_LIMIT);
@@ -72,11 +76,18 @@ export function GuildMembersSection({
/>
<span className="min-w-0 flex-1">
{/* Оформление участника приходит уже вычисленным (AGENT.md 7.2). */}
<Nickname
name={name}
cosmetics={member.cosmetics}
className="max-w-full text-sm font-medium"
/>
<span className="flex items-center gap-1">
<Nickname
name={name}
cosmetics={member.cosmetics}
className="max-w-full text-sm font-medium"
/>
<SystemBadges
badges={member.badges}
isInstanceAdmin={member.is_instance_admin}
isOwner={member.user_id === ownerId}
/>
</span>
<span className="block truncate text-xs text-fg-muted">
{member.nickname === undefined || member.nickname === ''
? `@${member.username}`
@@ -212,6 +212,7 @@ function GuildSettingsBody({ guilds }: { guilds: { id: string }[] }) {
canBan={canBan}
canTimeout={canModerateMembers('TIMEOUT_MEMBERS')}
currentUserId={currentUser.data?.id}
ownerId={guild.owner_id}
/>
<GuildCosmeticsSection
key={`${guild.id}-cosmetics`}