merged with fixes

This commit is contained in:
onysd 2026-09-09 02:49:30 +03:00
parent a9e758b712
commit 2f1818d656
176 changed files with 9000 additions and 907 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -242,6 +242,7 @@ export const api = {
gifCatalogDocumentPreviewURL: (documentID: string) => `/api/gif-catalog/documents/${encodeURIComponent(documentID)}/preview`,
createStickerSet: (form: FormData) => request<CommandResult>("/api/actions/create-sticker-set", { method: "POST", body: form }),
setAccountAvatar: (form: FormData) => request<CommandResult>("/api/actions/set-account-avatar", { method: "POST", body: form }),
setAccountAvatarVideo: (form: FormData) => request<CommandResult>("/api/actions/set-account-avatar-video", { method: "POST", body: form }),
setChannelAvatar: (form: FormData) => request<CommandResult>("/api/actions/set-channel-avatar", { method: "POST", body: form }),
addStickerToSet: (form: FormData) => request<CommandResult>("/api/actions/add-sticker-to-set", { method: "POST", body: form }),
gifCatalog: () => request<GifCatalogListResponse>("/api/gif-catalog"),

View file

@ -13,10 +13,13 @@ type AvatarModalKind = "user" | "channel";
export function AvatarModal({ kind, id, onClose, onDone }: { kind: AvatarModalKind; id: number; onClose: () => void; onDone: () => void }) {
const [file, setFile] = useState<File | null>(null);
const [previewURL, setPreviewURL] = useState("");
const [videoStartTs, setVideoStartTs] = useState("0");
const [reason, setReason] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const isVideo = kind === "user" && !!file && file.type.startsWith("video/");
useEffect(() => {
if (!file) {
setPreviewURL("");
@ -29,7 +32,7 @@ export function AvatarModal({ kind, id, onClose, onDone }: { kind: AvatarModalKi
async function submit() {
if (!file) {
setError("Choose an image file first.");
setError("Choose an image or video file first.");
return;
}
if (!reason.trim()) {
@ -41,9 +44,13 @@ export function AvatarModal({ kind, id, onClose, onDone }: { kind: AvatarModalKi
try {
const idField = kind === "channel" ? "channel_id" : "user_id";
const form = new FormData();
form.set("metadata", JSON.stringify({ command_id: "", reason: reason.trim(), confirm: true, [idField]: id }));
const metadata: Record<string, unknown> = { command_id: "", reason: reason.trim(), confirm: true, [idField]: id };
if (isVideo) {
metadata.video_start_ts = Number(videoStartTs) || 0;
}
form.set("metadata", JSON.stringify(metadata));
form.set("file", file, file.name);
const result = kind === "channel" ? await api.setChannelAvatar(form) : await api.setAccountAvatar(form);
const result = kind === "channel" ? await api.setChannelAvatar(form) : isVideo ? await api.setAccountAvatarVideo(form) : await api.setAccountAvatar(form);
if (result.error) {
setError(result.error);
return;
@ -71,11 +78,32 @@ export function AvatarModal({ kind, id, onClose, onDone }: { kind: AvatarModalKi
</div>
<div className="command-body">
<label className={`gift-file-picker ${file ? "has-file" : ""}`}>
<input type="file" accept="image/png,image/jpeg,image/webp" onChange={(event) => setFile(event.target.files?.[0] ?? null)} />
{previewURL ? <img className="gift-file-icon" src={previewURL} alt="" style={{ objectFit: "cover" }} /> : <ImagePlus size={22} />}
<span className="gift-file-copy"><span className="gift-field-label">{"New avatar"}</span><strong>{file ? file.name : "Choose a JPEG, PNG, or WebP image"}</strong></span>
<input
type="file"
accept={kind === "user" ? "image/png,image/jpeg,image/webp,video/mp4" : "image/png,image/jpeg,image/webp"}
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
/>
{previewURL ? (
isVideo ? (
<video className="gift-file-icon" src={previewURL} style={{ objectFit: "cover" }} muted loop autoPlay />
) : (
<img className="gift-file-icon" src={previewURL} alt="" style={{ objectFit: "cover" }} />
)
) : (
<ImagePlus size={22} />
)}
<span className="gift-file-copy">
<span className="gift-field-label">{"New avatar"}</span>
<strong>{file ? file.name : kind === "user" ? "Choose a JPEG, PNG, WebP image, or MP4 video" : "Choose a JPEG, PNG, or WebP image"}</strong>
</span>
<span className="gift-file-action">{file ? "Change file" : "Choose file"}</span>
</label>
{isVideo && (
<label className="gift-reason-field">
<span>{"Video start (seconds)"}</span>
<input type="number" min="0" step="0.1" value={videoStartTs} onChange={(event) => setVideoStartTs(event.target.value)} />
</label>
)}
<label className="gift-reason-field"><span>{"Audit reason"}</span><input value={reason} placeholder={"Briefly describe why this avatar is being changed"} onChange={(event) => setReason(event.target.value)} /></label>
{error && <Alert>{error}</Alert>}
</div>