164 lines
5.6 KiB
TypeScript
164 lines
5.6 KiB
TypeScript
|
|
import { useState } from 'react';
|
||
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import { useNavigate } from 'react-router';
|
||
|
|
|
||
|
|
import { deleteGuild, guildQueryKey, leaveGuild } from '@/api/guilds';
|
||
|
|
import { instanceQueryKey } from '@/api/instance';
|
||
|
|
import { myGuildsQueryKey } from '@/api/users';
|
||
|
|
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||
|
|
import { Modal } from '@/components/ui/Modal';
|
||
|
|
import { Button } from '@/components/ui/primitives';
|
||
|
|
import { useSessionStore } from '@/stores/session';
|
||
|
|
import { SettingsSection } from '@/pages/settings/SettingsSection';
|
||
|
|
|
||
|
|
interface GuildDangerSectionProps {
|
||
|
|
guildId: string;
|
||
|
|
guildName: string;
|
||
|
|
/** Владелец не может покинуть сервер — только удалить его. */
|
||
|
|
isOwner: boolean;
|
||
|
|
/** Администратор инстанса может удалить любой сервер. */
|
||
|
|
isInstanceAdmin: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
type ConfirmAction = 'leave' | 'delete';
|
||
|
|
|
||
|
|
/** «Опасная зона»: выход с сервера и удаление сервера с подтверждением. */
|
||
|
|
export function GuildDangerSection({
|
||
|
|
guildId,
|
||
|
|
guildName,
|
||
|
|
isOwner,
|
||
|
|
isInstanceAdmin,
|
||
|
|
}: GuildDangerSectionProps) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const [confirm, setConfirm] = useState<ConfirmAction | null>(null);
|
||
|
|
// Последнее действие: пока окно закрывается, тексты не должны «прыгать».
|
||
|
|
const [lastAction, setLastAction] = useState<ConfirmAction>('leave');
|
||
|
|
|
||
|
|
const ask = (action: ConfirmAction): void => {
|
||
|
|
setLastAction(action);
|
||
|
|
setConfirm(action);
|
||
|
|
};
|
||
|
|
|
||
|
|
/** После выхода/удаления возвращаемся в приложение и обновляем снапшот. */
|
||
|
|
const afterExit = (): void => {
|
||
|
|
setConfirm(null);
|
||
|
|
useSessionStore.getState().removeGuild(guildId);
|
||
|
|
useSessionStore.getState().selectSettingsGuild(null);
|
||
|
|
void queryClient.invalidateQueries({ queryKey: myGuildsQueryKey });
|
||
|
|
void queryClient.invalidateQueries({ queryKey: instanceQueryKey });
|
||
|
|
void queryClient.removeQueries({ queryKey: guildQueryKey(guildId) });
|
||
|
|
void navigate('/app', { replace: true });
|
||
|
|
};
|
||
|
|
|
||
|
|
const leave = useMutation({
|
||
|
|
mutationFn: () => leaveGuild(guildId),
|
||
|
|
onSuccess: afterExit,
|
||
|
|
});
|
||
|
|
|
||
|
|
const remove = useMutation({
|
||
|
|
mutationFn: () => deleteGuild(guildId),
|
||
|
|
onSuccess: afterExit,
|
||
|
|
});
|
||
|
|
|
||
|
|
const canDelete = isOwner || isInstanceAdmin;
|
||
|
|
const pending = leave.isPending || remove.isPending;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SettingsSection
|
||
|
|
title={t('settings.servers.danger.title')}
|
||
|
|
description={t('settings.servers.danger.description')}
|
||
|
|
className="border-danger/40"
|
||
|
|
>
|
||
|
|
<div className="mt-4 flex flex-col gap-4">
|
||
|
|
{isOwner ? (
|
||
|
|
<p className="text-sm text-fg-muted">{t('settings.servers.danger.leaveOwnerHint')}</p>
|
||
|
|
) : (
|
||
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
|
|
<p className="max-w-md text-sm text-fg-muted">
|
||
|
|
{t('settings.servers.danger.leaveHint')}
|
||
|
|
</p>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
className="border-danger/50 text-danger"
|
||
|
|
disabled={pending}
|
||
|
|
onClick={() => {
|
||
|
|
leave.reset();
|
||
|
|
ask('leave');
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{t('settings.servers.danger.leave')}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{canDelete ? (
|
||
|
|
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border/40 pt-4">
|
||
|
|
<p className="max-w-md text-sm text-fg-muted">
|
||
|
|
{t('settings.servers.danger.deleteHint')}
|
||
|
|
</p>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
className="border-danger/50 text-danger"
|
||
|
|
disabled={pending}
|
||
|
|
onClick={() => {
|
||
|
|
remove.reset();
|
||
|
|
ask('delete');
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{t('settings.servers.danger.delete')}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{leave.isError ? <ErrorNotice className="mt-3" error={leave.error} /> : null}
|
||
|
|
{remove.isError ? <ErrorNotice className="mt-3" error={remove.error} /> : null}
|
||
|
|
|
||
|
|
<Modal
|
||
|
|
open={confirm !== null}
|
||
|
|
title={t(
|
||
|
|
lastAction === 'delete'
|
||
|
|
? 'settings.servers.danger.deleteConfirmTitle'
|
||
|
|
: 'settings.servers.danger.leaveConfirmTitle',
|
||
|
|
)}
|
||
|
|
onClose={() => setConfirm(null)}
|
||
|
|
footer={
|
||
|
|
<>
|
||
|
|
<Button variant="ghost" onClick={() => setConfirm(null)}>
|
||
|
|
{t('common.cancel')}
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
disabled={pending}
|
||
|
|
onClick={() => {
|
||
|
|
if (lastAction === 'delete') {
|
||
|
|
remove.mutate();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
leave.mutate();
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{t(
|
||
|
|
lastAction === 'delete'
|
||
|
|
? 'settings.servers.danger.deleteConfirm'
|
||
|
|
: 'settings.servers.danger.leaveConfirm',
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<p className="text-sm text-fg-muted">
|
||
|
|
{t(
|
||
|
|
lastAction === 'delete'
|
||
|
|
? 'settings.servers.danger.deleteConfirmBody'
|
||
|
|
: 'settings.servers.danger.leaveConfirmBody',
|
||
|
|
{ name: guildName },
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
</Modal>
|
||
|
|
</SettingsSection>
|
||
|
|
);
|
||
|
|
}
|