200 lines
7.3 KiB
TypeScript
200 lines
7.3 KiB
TypeScript
import { useForm } from '@inertiajs/react';
|
|
import { useRef, useState } from 'react';
|
|
import { createOperationKey } from './operation-key';
|
|
type Data = {
|
|
body: string;
|
|
file: File | null;
|
|
shared: boolean;
|
|
client_key: string;
|
|
};
|
|
function object(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
/** Keeps the original command, including the immutable File object, until delivery is confirmed. */
|
|
export function useRoomDelivery({
|
|
roomId,
|
|
actorId,
|
|
kind,
|
|
mutable,
|
|
canWrite,
|
|
}: {
|
|
roomId: number;
|
|
actorId: number;
|
|
kind: 'message' | 'upload';
|
|
mutable: boolean;
|
|
canWrite: boolean;
|
|
}) {
|
|
const form = useForm<Data>({
|
|
body: '',
|
|
file: null,
|
|
shared: false,
|
|
client_key: createOperationKey(),
|
|
});
|
|
const live = useRef(form.data);
|
|
live.current = form.data;
|
|
const generation = useRef({ body: 0, file: 0, shared: 0 });
|
|
const identity = useRef(JSON.stringify([roomId, actorId, kind]));
|
|
const observed = useRef(identity.current);
|
|
observed.current = JSON.stringify([roomId, actorId, kind]);
|
|
const pending = useRef<{
|
|
data: Data;
|
|
generation: typeof generation.current;
|
|
ambiguous: boolean;
|
|
} | null>(null);
|
|
const [problem, setProblem] = useState<string | null>(null);
|
|
const recovering = !!pending.current?.ambiguous;
|
|
const invalid = observed.current !== identity.current;
|
|
const dirty =
|
|
!!pending.current ||
|
|
(kind === 'message' ? !!form.data.body : !!form.data.file);
|
|
const blocked =
|
|
form.processing || invalid || !canWrite || (!mutable && !recovering);
|
|
function change<K extends 'body' | 'file' | 'shared'>(
|
|
key: K,
|
|
value: Data[K],
|
|
) {
|
|
generation.current[key]++;
|
|
live.current = { ...live.current, [key]: value };
|
|
form.setData(live.current);
|
|
}
|
|
function submit() {
|
|
if (
|
|
blocked ||
|
|
(kind === 'upload' &&
|
|
!pending.current?.data.file &&
|
|
!live.current.file)
|
|
)
|
|
return;
|
|
const command = pending.current ?? {
|
|
data: { ...live.current },
|
|
generation: { ...generation.current },
|
|
ambiguous: false,
|
|
};
|
|
pending.current = command;
|
|
setProblem(null);
|
|
form.transform(() =>
|
|
kind === 'message'
|
|
? {
|
|
body: command.data.body,
|
|
client_key: command.data.client_key,
|
|
}
|
|
: {
|
|
file: command.data.file,
|
|
shared: command.data.shared,
|
|
client_key: command.data.client_key,
|
|
},
|
|
);
|
|
const unknown = () => {
|
|
if (pending.current) pending.current.ambiguous = true;
|
|
setProblem(
|
|
'O envio não foi confirmado. A tentativa original e a sua preparação foram conservadas.',
|
|
);
|
|
return false;
|
|
};
|
|
form.post(
|
|
`/parcerias/salas/${roomId}/${kind === 'message' ? 'mensagens' : 'documentos'}`,
|
|
{
|
|
preserveScroll: true,
|
|
forceFormData: kind === 'upload',
|
|
onSuccess: (page) => {
|
|
if (observed.current !== identity.current) return;
|
|
const props = page.props;
|
|
const room = props.room;
|
|
const flash = props.flash;
|
|
const confirmed =
|
|
object(room) &&
|
|
room.id === roomId &&
|
|
(kind === 'message'
|
|
? Array.isArray(props.messages) &&
|
|
props.messages.some(
|
|
(message) =>
|
|
object(message) &&
|
|
message.author_id === actorId &&
|
|
message.client_key ===
|
|
command.data.client_key &&
|
|
message.body === command.data.body.trim(),
|
|
)
|
|
: object(flash) &&
|
|
typeof flash.uploaded_document_id === 'string' &&
|
|
Array.isArray(props.documents) &&
|
|
props.documents.some(
|
|
(document) =>
|
|
object(document) &&
|
|
document.document_id ===
|
|
flash.uploaded_document_id &&
|
|
document.owner_id === actorId &&
|
|
document.file_name ===
|
|
command.data.file?.name,
|
|
));
|
|
if (!confirmed) {
|
|
unknown();
|
|
return;
|
|
}
|
|
const next: Data = {
|
|
body: '',
|
|
file: null,
|
|
shared: false,
|
|
client_key: createOperationKey(),
|
|
};
|
|
if (
|
|
kind === 'message' &&
|
|
generation.current.body !== command.generation.body
|
|
)
|
|
next.body = live.current.body;
|
|
if (kind === 'upload') {
|
|
if (
|
|
generation.current.file !== command.generation.file
|
|
) {
|
|
next.file = live.current.file;
|
|
next.shared = live.current.shared;
|
|
} else if (
|
|
generation.current.shared !==
|
|
command.generation.shared
|
|
)
|
|
next.shared = live.current.shared;
|
|
}
|
|
pending.current = null;
|
|
live.current = next;
|
|
form.setDefaults({
|
|
body: '',
|
|
file: null,
|
|
shared: false,
|
|
client_key: next.client_key,
|
|
});
|
|
form.setData(next);
|
|
form.clearErrors();
|
|
setProblem(null);
|
|
},
|
|
onError: () => {
|
|
if (!pending.current?.ambiguous) pending.current = null;
|
|
},
|
|
onHttpException: (response) => {
|
|
if (
|
|
response.status >= 500 ||
|
|
response.status === 409 ||
|
|
pending.current?.ambiguous
|
|
)
|
|
return unknown();
|
|
pending.current = null;
|
|
setProblem(
|
|
'Não foi possível confirmar o envio com o acesso actual. A preparação foi conservada.',
|
|
);
|
|
return false;
|
|
},
|
|
onNetworkError: unknown,
|
|
onCancel: unknown,
|
|
},
|
|
);
|
|
}
|
|
return {
|
|
form,
|
|
data: form.data,
|
|
change,
|
|
submit,
|
|
problem,
|
|
recovering,
|
|
dirty,
|
|
blocked,
|
|
};
|
|
}
|