feat(social): блокировка пользователей

- PUT/DELETE /users/@me/blocks/{id}: блокировка снимает дружбу и заявки в обе
  стороны, разблокировка возвращает возможность писать (AGENT.md 7.2)
- блокировка запрещает личные сообщения (ошибка dm.blocked), открытие беседы и
  заявки в друзья с любой стороны
- клиент: раздел «Заблокированные» в друзьях со снятием блокировки, действие
  «Заблокировать» в меню строки друга и в меню модерации участника
- тесты: Go (блокировка, заявки, беседа, сообщение) и Vitest (список, снятие,
  блокировка из меню)
This commit is contained in:
2026-09-21 19:21:00 +03:00
parent afcdf9c1c4
commit ed98c78c9e
11 changed files with 461 additions and 5 deletions
@@ -9,7 +9,9 @@ import {
kickGuildMember,
timeoutGuildMember,
} from '@/api/moderation';
import { blockUser } from '@/api/friends';
import { Menu, MenuGroupLabel, MenuItem, MenuSeparator } from '@/components/ui/Menu';
import { useFriendsStore } from '@/stores/friends';
import { useApiErrorMessage } from '@/lib/useApiErrorMessage';
/** Пресеты тайм-аута в минутах: 0 — снять ограничение. */
@@ -64,6 +66,17 @@ export function MemberModerationMenu({
close();
},
});
const reloadRelationships = useFriendsStore((state) => state.reload);
const blockedUsers = useFriendsStore((state) => state.blocked);
const blocked = blockedUsers.some((entry) => entry.user_id === userId);
const block = useMutation({
mutationFn: () => blockUser(userId),
onSuccess: () => {
void reloadRelationships();
},
});
const kick = useMutation({
mutationFn: () => kickGuildMember(guildId, userId),
onSuccess: () => {
@@ -162,6 +175,16 @@ export function MemberModerationMenu({
</MenuItem>
) : null}
{/* Личная блокировка не зависит от прав на сервере (AGENT.md 7.2). */}
<MenuItem
danger
disabled={pending || blocked}
data-testid={`member-block-${userId}`}
onClick={() => block.mutate()}
>
{t(blocked ? 'friends.blocked.alreadyBlocked' : 'friends.list.block')}
</MenuItem>
{error === null || error === undefined ? null : (
<p className="px-2 py-1 text-xs text-danger">{describeError(error)}</p>
)}