import React, { useState, useRef, useEffect, useContext } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { createTweet, buildCSRFHeaders } from "../ash_rpc"; import { uploadFile } from "../upload"; import { AuthCtx } from "../context"; import { Avatar, CharCount } from "./ui"; const MAX = 280; export function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) { const { username, displayName, email, avatarUrl } = useContext(AuthCtx); const [text, setText] = useState(""); const [error, setError] = useState(null); const [pendingFile, setPendingFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [mediaId, setMediaId] = useState(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const textareaRef = useRef(null); const fileInputRef = useRef(null); const qc = useQueryClient(); const mutation = useMutation({ mutationFn: async (content: string) => { const res = await createTweet({ input: { content, mediaId: mediaId ?? undefined }, fields: ["id", "content", "userId", "state"], headers: buildCSRFHeaders(), }); if (!res.success) throw new Error(res.errors?.[0]?.message ?? "Failed"); return res.data; }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["tweets"] }); qc.invalidateQueries({ queryKey: ["following_tweets"] }); setText(""); setError(null); setMediaId(null); setPendingFile(null); setUploadError(null); if (previewUrl) { URL.revokeObjectURL(previewUrl); setPreviewUrl(null); } onSuccess?.(); }, onError: (e: Error) => setError(e.message), }); async function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; e.target.value = ""; if (previewUrl) URL.revokeObjectURL(previewUrl); const localUrl = URL.createObjectURL(file); setPendingFile(file); setPreviewUrl(localUrl); setMediaId(null); setUploadError(null); setUploading(true); const csrfToken = buildCSRFHeaders()["X-CSRF-Token"] as string; const result = await uploadFile(file, csrfToken); setUploading(false); if ("error" in result) { setUploadError(result.error); setPendingFile(null); URL.revokeObjectURL(localUrl); setPreviewUrl(null); } else { setMediaId(result.mediaId); } } function removeAttachment() { if (previewUrl) URL.revokeObjectURL(previewUrl); setPendingFile(null); setPreviewUrl(null); setMediaId(null); setUploadError(null); } function handleKeyDown(e: React.KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); submit(); } } function submit() { const trimmed = text.trim(); if (!trimmed) return; if (trimmed.length > MAX) { setError(`Max ${MAX} characters`); return; } setError(null); mutation.mutate(trimmed); } useEffect(() => { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; el.style.height = `${el.scrollHeight}px`; }, [text]); return (