import { useEffect, useRef, useState } from "react"; import { INK, RED, BLUE, PAPER, SKY, MUTE, FONT_MONO, FONT_SERIF } from "../../theme"; import { MIN_CHARS, PHOTO_ALT_MAX } from "../../constants"; import { preparePhoto } from "../../lib/image"; import PhotoFrame from "../../components/PhotoFrame"; /* * Brief schrijven: minimumlengte + hoogstens één foto. Dit is een tabblad van de correspondentie, * geen dialoog — je kunt ernaast blijven lezen en zoeken zonder je concept kwijt te raken. Daarom * blijft dit paneel gemonteerd terwijl je op het andere tabblad kijkt (`active` zet alleen display), * en gooit alleen ThreadView het concept weg, door de sleutel te verversen na verzenden. * * canPhoto = false zolang de ander mij nog nooit geschreven heeft: dan is er geen fotokiezer, alleen * een regel die zegt waarom. Ongevraagde post is tekst. De server weigert zo'n foto ook (dit is niet * de enige slotgracht), maar dan is hij al gekozen en verkleind — vandaar dat de knop hier wegblijft. * * onSend(text, photo) → Promise, waarbij photo null is of { data, mime, width, height, alt } — al * verkleind en van EXIF ontdaan (zie lib/image.js). De beschrijving is verplicht: zonder gaat de * brief niet weg. onDraftChange(bool) laat ThreadView weten of er iets te verliezen valt. */ export default function ComposePane({ recipient, canPhoto = false, active, onSend, onDraftChange }) { const [text, setText] = useState(""); const [photo, setPhoto] = useState(null); // { data, mime, width, height, previewUrl } const [alt, setAlt] = useState(""); const [loadingPhoto, setLoadingPhoto] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const fileInput = useRef(null); const left = MIN_CHARS - text.length; const describedPhoto = !photo || alt.trim().length > 0; const ready = left <= 0 && !sending && !loadingPhoto && describedPhoto; /* * Is er een begonnen brief? App vraagt daarop om bevestiging bij het verlaten. Het opruimen zet * het weer op false — ook als dit paneel verdwijnt (na verzenden, of bij een andere correspondent), * zodat er geen concept blijft rondspoken dat er niet meer is. */ const hasDraft = Boolean(text.trim() || photo); useEffect(() => { onDraftChange?.(hasDraft); return () => onDraftChange?.(false); }, [hasDraft, onDraftChange]); /* De blob-URL van het voorbeeld weer intrekken zodra de foto weg is of het paneel verdwijnt */ const dropPhoto = () => { if (photo) URL.revokeObjectURL(photo.previewUrl); setPhoto(null); setAlt(""); }; useEffect(() => () => { if (photo) URL.revokeObjectURL(photo.previewUrl); }, [photo]); /* Een gekozen bestand verkleinen en hercoderen; fouten komen uit lib/image.js met een leesbare tekst */ const pickPhoto = async (file) => { if (!file) return; setLoadingPhoto(true); setError(null); try { const prepared = await preparePhoto(file); dropPhoto(); setPhoto(prepared); } catch (e) { setError(e.message); } finally { setLoadingPhoto(false); if (fileInput.current) fileInput.current.value = ""; // dezelfde foto opnieuw kiezen mag } }; const submit = async () => { if (!ready) return; setSending(true); setError(null); try { await onSend(text, photo && { ...photo, alt }); // gelukt: ThreadView springt terug naar de brieven en ververst dit paneel } catch (e) { setError(e.message); setSending(false); } }; return (

aan {recipient.name} · minimaal {MIN_CHARS} tekens{canPhoto && " · maximaal één foto"}