import { DownloadSimpleIcon, ImageIcon, LockIcon, SpinnerGapIcon, TrayIcon, } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { api } from "../api/apiClient"; import { type CanvasTools, ComposeCanvas, } from "../components/ui/ComposeCanvas"; import DateDisplay from "../components/ui/DateDisplay"; import { endpoints } from "../config/endpoints"; import { PATHS } from "../config/routes"; import { useKeyStore } from "../store/useKeyStore"; import { CryptoUtils } from "../utils/crypto"; import { decryptCanvasImages, encryptCanvasImages } from "../utils/letterLogic"; export default function Editor() { const navigate = useNavigate(); const { public_id } = useParams(); const letterIdRef = useRef(public_id ?? ""); const [isInitialLoading, setIsInitialLoading] = useState(false); const [isSealing, setIsSealing] = useState(false); const [isSaveSuccess, setIsSaveSuccess] = useState(false); const [shareLink, setShareLink] = useState(null); const [recipient, setRecipient] = useState(""); const { masterKey } = useKeyStore(); const canvasRef = useRef(null); const fileInputRef = useRef(null); useEffect(() => { if (!(public_id && masterKey)) return; const loadExistingLetter = async () => { setIsInitialLoading(true); const cryptoUtils = new CryptoUtils(); try { const res = await api.get(`${endpoints.LETTERS}${public_id}/`); const letterData = res.data; const metadata = await cryptoUtils.decryptMetadata( { encrypted_content: letterData.encrypted_metadata, encrypted_dek: letterData.encrypted_dek, }, masterKey, ); setRecipient(metadata.recipient || ""); const decryptedJsonStr = await cryptoUtils.decryptLetter( { encrypted_content: letterData.encrypted_content, encrypted_dek: letterData.encrypted_dek, }, masterKey, ); const canvasData = JSON.parse(decryptedJsonStr); await decryptCanvasImages( canvasData, letterData.images ?? [], letterData.encrypted_dek, masterKey, cryptoUtils, true, ); requestAnimationFrame(() => { canvasRef.current?.loadData(canvasData); }); } catch (_err) { } finally { setIsInitialLoading(false); } }; loadExistingLetter(); }, [public_id, masterKey]); // -------------------------------------------------------------------------------------- const handleImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const url = URL.createObjectURL(file); canvasRef.current?.addImage(url, file); } }; const handleSave = async (status: "SEALED" | "DRAFT"): Promise => { if (!(public_id || letterIdRef.current)) { letterIdRef.current = crypto.randomUUID(); navigate(PATHS.write(letterIdRef.current), { replace: true }); } else if (public_id) { letterIdRef.current = public_id; } if (isSealing || !masterKey) return; setIsSealing(true); const cryptoUtils = new CryptoUtils(); await cryptoUtils.initialize(); try { const canvasData = canvasRef.current?.getData() || { objects: [] }; const canvasImages = canvasRef.current?.getImages() || []; const encImageFilesMap = await encryptCanvasImages( canvasData, canvasImages, masterKey, cryptoUtils, ); const encrypted_letter = await cryptoUtils.encryptLetter( JSON.stringify(canvasData), masterKey, ); const encrypted_metadata = await cryptoUtils.encryptMetadata( { recipient, tags: [] }, masterKey, ); const formData = new FormData(); formData.append("public_id", letterIdRef.current); formData.append("type", "KEPT"); formData.append("status", status); formData.append("encrypted_content", encrypted_letter.encrypted_content); formData.append("encrypted_dek", encrypted_letter.encrypted_dek); formData.append( "encrypted_metadata", encrypted_metadata.encrypted_content, ); encImageFilesMap.forEach((blob, filename) => { formData.append("image_files", blob, filename); }); await api.put(`${endpoints.LETTERS}${letterIdRef.current}/`, formData); setIsSaveSuccess(true); if (status === "SEALED" && encrypted_letter.sharingKey) { const link = `${window.location.origin}${PATHS.read(letterIdRef.current)}#${encrypted_letter.sharingKey}`; setShareLink(link); } setTimeout(() => setIsSaveSuccess(false), 5000); } catch (_error) { } finally { setIsSealing(false); } }; const copyToClipboard = async () => { if (!shareLink) return; try { await navigator.clipboard.writeText(shareLink); } catch (_err) {} }; return (
{isInitialLoading && (

Opening your draft...

)} {shareLink && (

Sealed & Ready

This letter is now encrypted. Share this secret link with your recipient.

Zero-Knowledge: The key is in the link, not our servers.

)} {isSaveSuccess && !shareLink && (

Your letter is saved!

)} {isSealing && (

Securing your letter...

)}
setRecipient(e.target.value)} className="bg-transparent border-none outline-none text-2xl md:text-3xl lg:text-4xl font-serif text-base-content placeholder:text-base-content/10 w-full" />
); }