import React, { useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { MdContentCopy, MdRefresh, MdCheck, MdClose, MdAdd } from 'react-icons/md'; import { RiSendPlaneLine } from 'react-icons/ri'; import { useTranslation } from '@/hooks/useTranslation'; import { useAuth } from '@/context/AuthContext'; import { fetchWithAuth } from '@/utils/fetch'; import { getAPIBaseUrl } from '@/services/environment'; import { isInboxDrainEnabled, setInboxDrainEnabled } from '@/services/send/devicePrefs'; import { navigateToLogin } from '@/utils/nav'; import { eventDispatcher } from '@/utils/event'; import type { DBSendAllowedSender, DBSendInboxItem } from '@/types/sendRecords'; import SubPageHeader from '../SubPageHeader'; import { BoxedList, SectionTitle, SettingLabel, SettingsSwitchRow } from '../primitives'; interface SendToReadestFormProps { onBack: () => void; } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; /** Pull the editable slug out of a `{slug}-{token}@domain` address. */ function slugOf(address: string): string { const local = address.split('@')[0] ?? ''; const dash = local.lastIndexOf('-'); return dash > 0 ? local.slice(0, dash) : local; } /** The fixed `-{token}@domain` part shown after the editable slug. */ function suffixOf(address: string): string { return address.slice(slugOf(address).length); } const SendToReadestForm: React.FC = ({ onBack }) => { const _ = useTranslation(); const router = useRouter(); const { user } = useAuth(); const apiBase = getAPIBaseUrl(); const [address, setAddress] = useState(''); const [senders, setSenders] = useState([]); const [activity, setActivity] = useState([]); const [newEmail, setNewEmail] = useState(''); const [slugInput, setSlugInput] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [drainEnabled, setDrainEnabled] = useState(() => isInboxDrainEnabled()); // Editing affordances stay collapsed once configured, keeping the panel // minimal; the refresh / plus icons reveal the input rows. const [editingAddress, setEditingAddress] = useState(false); const [addingSender, setAddingSender] = useState(false); const toast = (message: string, type: 'info' | 'error' | 'success' = 'info') => eventDispatcher.dispatch('toast', { message, type, timeout: 2500 }); const toggleDrain = () => { const next = !drainEnabled; setDrainEnabled(next); setInboxDrainEnabled(next); }; const load = useCallback(async () => { try { const [addrRes, sendersRes, inboxRes] = await Promise.all([ fetchWithAuth(`${apiBase}/send/address`, { method: 'GET' }), fetchWithAuth(`${apiBase}/send/senders`, { method: 'GET' }), fetchWithAuth(`${apiBase}/send/inbox`, { method: 'GET' }), ]); const addrData = (await addrRes.json()) as { address: string }; const sendersData = (await sendersRes.json()) as { senders: DBSendAllowedSender[] }; const inboxData = (await inboxRes.json()) as { items: DBSendInboxItem[] }; setAddress(addrData.address); setSlugInput(slugOf(addrData.address)); setSenders(sendersData.senders); setActivity(inboxData.items); } catch { toast(_('Could not load Send to Readest settings'), 'error'); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiBase]); useEffect(() => { if (user) void load(); }, [user, load]); const copyAddress = async () => { try { await navigator.clipboard.writeText(address); toast(_('Address copied'), 'success'); } catch { toast(_('Could not copy address'), 'error'); } }; // Saving issues a fresh address: the chosen slug + a new token suffix. // (Leaving the slug unchanged and saving is also how you rotate a leaked // address — the token is always regenerated.) const saveAddress = async () => { const slug = slugInput.trim(); if (!slug) { toast(_('Enter a name for your address'), 'error'); return; } setSaving(true); try { const res = await fetchWithAuth(`${apiBase}/send/address`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug }), }); const data = (await res.json()) as { address: string }; setAddress(data.address); setSlugInput(slugOf(data.address)); setEditingAddress(false); toast(_('Address updated'), 'success'); } catch (err) { toast(err instanceof Error ? err.message : _('Could not update address'), 'error'); } finally { setSaving(false); } }; const addSender = async () => { const email = newEmail.trim().toLowerCase(); if (!EMAIL_RE.test(email)) { toast(_('Enter a valid email address'), 'error'); return; } try { const res = await fetchWithAuth(`${apiBase}/send/senders`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); const data = (await res.json()) as { sender: DBSendAllowedSender }; setSenders((prev) => [...prev.filter((s) => s.id !== data.sender.id), data.sender]); setNewEmail(''); setAddingSender(false); } catch { toast(_('Could not add sender'), 'error'); } }; const approveSender = async (id: string) => { try { await fetchWithAuth(`${apiBase}/send/senders`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); setSenders((prev) => prev.map((s) => (s.id === id ? { ...s, status: 'approved' } : s))); } catch { toast(_('Could not approve sender'), 'error'); } }; const removeSender = async (id: string) => { try { await fetchWithAuth(`${apiBase}/send/senders`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); setSenders((prev) => prev.filter((s) => s.id !== id)); } catch { toast(_('Could not remove sender'), 'error'); } }; return (
{!user ? (

{_('Sign in to send books and articles to your library.')}

) : loading ? (
{[ { key: 'address', card: 'h-28' }, { key: 'senders', card: 'h-32' }, { key: 'activity', card: 'h-24' }, ].map((section) => (
))}
) : (
{_('Your inbound address')}
{address} {address && ( )}
{(editingAddress || !address) && (
setSlugInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void saveAddress(); }} aria-label={_('Customize your address name')} placeholder={_('your-name')} /> {suffixOf(address)}
)}

{_('Email a book or document to this address from an approved sender below.')}

{_('Approved senders')}
{senders.length === 0 && (
{_('No approved senders yet. Add an email to let it send to your library.')}
)} {senders.map((sender) => (
{sender.email} {sender.status === 'pending' && ( {_('Pending approval')} )}
{sender.status === 'pending' && ( )}
))}
{(addingSender || senders.length === 0) && (
setNewEmail(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void addSender(); }} />
)}
{_('Recent activity')}
{activity.length === 0 && (
{_('Nothing sent yet. Email a book to your address above.')}
)} {activity.map((item) => (
{item.filename || item.url || _('Untitled')} {item.status === 'done' && _('Added to your library')} {item.status === 'pending' && _('Waiting to be processed')} {item.status === 'claimed' && _('Processing…')} {item.status === 'failed' && (item.error || _('Failed'))}
))}
)}
); }; export default SendToReadestForm;