fix(files): аватары и баннеры доступны авторизованным пользователям
Аватар нельзя было показать в списке друзей: файл без комнаты был доступен только загрузившему. Добавлено назначение файла (`files.purpose`: attachment | avatar | banner, миграция 00006): аватары и баннеры отдаются любому авторизованному пользователю, вложения по-прежнему проверяются по правам комнаты, а непривязанные загрузки остаются приватными.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { createGuild } from '@/api/guilds';
|
||||
import { instanceQueryKey } from '@/api/instance';
|
||||
import { myGuildsQueryKey } from '@/api/users';
|
||||
import { ErrorNotice } from '@/components/ui/ErrorNotice';
|
||||
import { Field } from '@/components/ui/Field';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/primitives';
|
||||
import { useInstance } from '@/lib/hooks';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
import { useUiStore } from '@/stores/ui';
|
||||
|
||||
/**
|
||||
* Единственное модальное окно создания сервера (`POST /guilds {name}`).
|
||||
* Открывается из рейки и с пустого экрана «У вас пока нет серверов»,
|
||||
* поэтому смонтировано в `AppLayout`, а состояние — в `ui`-сторе.
|
||||
*/
|
||||
export function CreateGuildModal() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const instance = useInstance();
|
||||
|
||||
const open = useUiStore((state) => state.guildCreateOpen);
|
||||
const close = useUiStore((state) => state.closeGuildCreate);
|
||||
const guilds = useSessionStore((state) => state.guilds);
|
||||
const upsertGuild = useSessionStore((state) => state.upsertGuild);
|
||||
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: (guildName: string) => createGuild(guildName),
|
||||
onSuccess: (guild) => {
|
||||
close();
|
||||
setName('');
|
||||
upsertGuild(guild);
|
||||
void queryClient.invalidateQueries({ queryKey: myGuildsQueryKey });
|
||||
void queryClient.invalidateQueries({ queryKey: instanceQueryKey });
|
||||
void navigate(`/app/${guild.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
const limitReached =
|
||||
instance.data !== undefined && guilds.length >= instance.data.max_guilds_per_user;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={t('guilds.create.title')}
|
||||
onClose={close}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={close}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => create.mutate(name.trim())}
|
||||
disabled={create.isPending || name.trim().length === 0}
|
||||
>
|
||||
{t(create.isPending ? 'common.creating' : 'guilds.create.submit')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (name.trim() !== '') {
|
||||
create.mutate(name.trim());
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
label={t('guilds.create.name')}
|
||||
placeholder={t('guilds.create.namePlaceholder')}
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
maxLength={64}
|
||||
hint={t('guilds.create.hint')}
|
||||
/>
|
||||
{instance.data === undefined ? null : (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t('guilds.create.limit', { count: instance.data.max_guilds_per_user })}
|
||||
</p>
|
||||
)}
|
||||
{limitReached && !create.isError ? (
|
||||
<p className="text-xs text-warning" role="status">
|
||||
{t('guilds.create.limitReached')}
|
||||
</p>
|
||||
) : null}
|
||||
{create.isError ? <ErrorNotice error={create.error} /> : null}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user