feat(files): загрузка и выдача вложений
- POST /api/v1/channels/{id}/files (multipart, ATTACH_FILES): файл пишется на
диск под идентификатором, в БД хранятся метаданные и sha256; имя файла
очищается от путей и управляющих символов, объём ограничен MAX_UPLOAD_SIZE;
- GET/HEAD /files/{id}: содержимое с проверкой прав на комнату сообщения
(невидимая комната и чужие вложения → 404), ETag, immutable-кэш,
Content-Disposition с RFC 5987 для не-ASCII имён;
- вложения привязываются к сообщению при отправке (attachment_ids), сироты
ищутся через store.ListOrphanFiles для обслуживания;
- GET /api/v1/files/{id} — метаданные файла для клиента;
- chi-ручки теперь умеют отдавать huma-ошибки в общем конверте
(writeHumaAPIError), иначе 404 превращался в 500;
- READY отдаёт реальные read states пользователя.
Тесты: загрузка, скачивание участником, скрытая комната → 404, вложение в
сообщении, проверка подписи ETag.
This commit is contained in:
@@ -27,23 +27,30 @@ export function Modal({ open, title, onClose, children, footer }: ModalProps) {
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const [mounted, setMounted] = useState(open);
|
||||
const [closing, setClosing] = useState(false);
|
||||
const [prevOpen, setPrevOpen] = useState(open);
|
||||
// Свежий onClose без перезапуска эффекта фокуса при каждом рендере.
|
||||
const onCloseRef = useRef(onClose);
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose;
|
||||
}, [onClose]);
|
||||
|
||||
// Монтирование/размонтирование с учётом анимации закрытия.
|
||||
useEffect(() => {
|
||||
// Подстройка состояния под изменившийся проп во время рендера (документированный
|
||||
// приём React): эффект дал бы лишний кадр без анимации появления.
|
||||
if (prevOpen !== open) {
|
||||
setPrevOpen(open);
|
||||
if (open) {
|
||||
setMounted(true);
|
||||
setClosing(false);
|
||||
} else if (mounted) {
|
||||
setClosing(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Узел уходит из DOM после обратной анимации; при reduced-motion — сразу.
|
||||
useEffect(() => {
|
||||
if (open || !mounted) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setClosing(true);
|
||||
const timer = window.setTimeout(() => {
|
||||
setMounted(false);
|
||||
setClosing(false);
|
||||
|
||||
Reference in New Issue
Block a user