import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { Box, Alert, Autocomplete, Chip, FormControl, InputLabel, MenuItem, Pagination, Paper, Select, Stack, TextField, Typography, Button, } from "@mui/material"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import { api, getApiErrorMessage } from "../api"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import GmailReviewPage from "./GmailReviewPage"; import ViewStateNotice from "../components/ViewStateNotice"; import { PageSection } from "../layout/PageLayout"; import type { ViewResourceError } from "../hooks/useViewResource"; export type CorrespondenceInboxItem = { id: number; jobApplicationId: number; companyName?: string | null; jobTitle?: string | null; from: string; direction?: string | null; subject?: string | null; channel?: string | null; date: string; contentPreview: string; externalThreadId?: string | null; externalMessageId?: string | null; provider?: string | null; externalFrom?: string | null; externalTo?: string | null; labelCount: number; attachmentCount: number; }; type CorrespondenceInboxPage = { items: CorrespondenceInboxItem[]; page: number; pageSize: number; total: number; totalPages: number; }; type EmailProviderStatus = { provider: string; displayName: string; connected: boolean; address?: string | null; canRead: boolean; canSend: boolean; }; type JobChoice = { id: number; jobTitle: string; companyName: string; }; type EmailMessageDetail = { id: string; threadId: string; subject: string; from: string; to: string; date?: string | null; snippet: string; bodyText: string; labels: string[]; attachments: Array<{ fileName?: string | null; mimeType?: string | null; sizeBytes?: number | null }>; }; type EmailDraft = { id?: string; revision?: number; jobApplicationId: number; companyName: string; jobTitle: string; provider: string; providerName: string; fromAddress: string; to: string; subject: string; bodyText: string; threadId: string; clientRequestId: string; }; type StoredEmailDraft = Pick & { id: string; revision: number; createdAtUtc: string; updatedAtUtc: string; }; type EmailSendResult = { attemptId: string; status: "pending" | "sending" | "sent" | "failed" | "uncertain"; duplicate: boolean; externalMessageId?: string | null; externalThreadId?: string | null; failureCategory?: string | null; }; type DraftMutationError = ViewResourceError & { action: "save" | "discard" | "prepare" }; function readError(error: any): ViewResourceError { const status = error?.response?.status as number | undefined; const unauthorized = status === 401 || status === 403; return { kind: unauthorized ? "unauthorized" : "error", status, retryable: !unauthorized, message: getApiErrorMessage(error, "") }; } export default function CorrespondenceInboxPage() { const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const view = searchParams.get("view") === "review" ? "review" : "inbox"; const { toast } = useToast(); const { confirm } = useConfirm(); const { language, t } = useI18n(); const [items, setItems] = useState([]); const page = Number(searchParams.get("page")); const inboxPage = Number.isInteger(page) && page > 0 && page <= 2147483647 ? page : 1; const [inboxTotal, setInboxTotal] = useState(0); const [inboxTotalPages, setInboxTotalPages] = useState(0); const [providers, setProviders] = useState([]); const [providerStatusLoaded, setProviderStatusLoaded] = useState(false); const [jobs, setJobs] = useState([]); const [jobsLoaded, setJobsLoaded] = useState(false); const [jobsLoading, setJobsLoading] = useState(false); const [jobsError, setJobsError] = useState(null); const [loadedJobQuery, setLoadedJobQuery] = useState(null); const jobsRequest = useRef(0); const pendingJobs = useRef(null); const jobsPanel = useRef(null); const [jobQuery, setJobQuery] = useState(""); const [composeSetupOpen, setComposeSetupOpen] = useState(false); const [composeJobId, setComposeJobId] = useState(""); const [composeProvider, setComposeProvider] = useState(""); const [providerStatusError, setProviderStatusError] = useState(false); const [providerError, setProviderError] = useState(null); const [providersLoading, setProvidersLoading] = useState(false); const providerPending = useRef(false); const [selectedMessageId, setSelectedMessageId] = useState(null); const [messageDetail, setMessageDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailError, setDetailError] = useState(null); const [detailNotice, setDetailNotice] = useState(false); const detailRequest = useRef(0); const detailItem = useRef(null); const detailPending = useRef(false); const detailPanel = useRef(null); const inboxPanel = useRef(null); const providerPanel = useRef(null); const rowId = useId(); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [hasLoaded, setHasLoaded] = useState(false); const listRequest = useRef(0); const pendingList = useRef(null); const [loadedListKey, setLoadedListKey] = useState(null); const query = searchParams.get("q") ?? ""; const direction = ["inbound", "outbound", "internal"].includes(searchParams.get("direction") ?? "") ? searchParams.get("direction")! : "all"; const linkState = ["linked", "manual"].includes(searchParams.get("linkState") ?? "") ? searchParams.get("linkState")! : "all"; const currentListKey = JSON.stringify([query.trim(), direction, linkState, inboxPage]); const hasCurrentInbox = loadedListKey === currentListKey && loadError?.kind !== "unauthorized"; const [reviewVisited, setReviewVisited] = useState(view === "review"); const mounted = useRef(true); const [draftsReadError, setDraftsReadError] = useState(null); const [draftsLoading, setDraftsLoading] = useState(false); const draftsPending = useRef(false); const draftsRequest = useRef(0); const draftsPanel = useRef(null); const [draft, setDraft] = useState(null); const draftRef = useRef(draft); draftRef.current = draft; const draftBaseline = useRef(null); const draftPanel = useRef(null); const composeOperation = useRef(false); const [composeBusy, setComposeBusy] = useState(false); const latestT = useRef(t); latestT.current = t; const [storedDrafts, setStoredDrafts] = useState([]); const [savingDraft, setSavingDraft] = useState(false); const [draftError, setDraftError] = useState(null); const [sendResult, setSendResult] = useState(null); const [sendError, setSendError] = useState(null); const [sending, setSending] = useState(false); const [unlinkingThreadId, setUnlinkingThreadId] = useState(null); const unlinkPending = useRef(false); const [unlinkError, setUnlinkError] = useState(null); const updateFilters = useCallback((changes: Record) => { const next = new URLSearchParams(searchParams); for (const [key, value] of Object.entries(changes)) { if (!value || (value === "all" && (key === "direction" || key === "linkState")) || (key === "page" && value === "1") || (key === "view" && value === "inbox")) next.delete(key); else next.set(key, value); } navigate({ pathname: location.pathname, search: `?${next}`, hash: location.hash }, { state: location.state, replace: Object.hasOwn(changes, "q") }); }, [location.hash, location.pathname, location.state, navigate, searchParams]); const latestFilters = useRef(updateFilters); latestFilters.current = updateFilters; useEffect(() => { mounted.current = true; return () => { mounted.current = false; listRequest.current += 1; detailRequest.current += 1; jobsRequest.current += 1; draftsRequest.current += 1; draftsPending.current = false; }; }, []); const load = useCallback(async () => { if (view !== "inbox") return; const key = JSON.stringify([query.trim(), direction, linkState, inboxPage]); if (pendingList.current === key) return; const request = ++listRequest.current; pendingList.current = key; setLoading(true); setLoadError(null); try { const res = await api.get("/correspondence/page", { params: { q: query.trim() || undefined, direction: direction === "all" ? undefined : direction, linkState: linkState === "all" ? undefined : linkState, page: inboxPage, pageSize: 50, }, }); if (!mounted.current || request !== listRequest.current) return; setItems(res.data?.items ?? []); setInboxTotal(res.data?.total ?? 0); setInboxTotalPages(res.data?.totalPages ?? 0); setLoadedListKey(key); if (res.data?.page && res.data.page !== inboxPage) latestFilters.current({ page: String(res.data.page) }); setHasLoaded(true); detailRequest.current += 1; detailItem.current = null; detailPending.current = false; setSelectedMessageId(null); setMessageDetail(null); } catch (error) { if (mounted.current && request === listRequest.current) setLoadError(readError(error)); } finally { if (mounted.current && request === listRequest.current) { pendingList.current = null; setLoading(false); } } }, [direction, inboxPage, linkState, query, view]); const latestLoad = useRef(load); latestLoad.current = load; useEffect(() => { if (view === "inbox") void load(); else setReviewVisited(true); return () => { listRequest.current += 1; pendingList.current = null; detailRequest.current += 1; detailItem.current = null; detailPending.current = false; setSelectedMessageId(null); setMessageDetail(null); setDetailLoading(false); }; }, [load, view]); const loadProviders = useCallback(async () => { if (providerPending.current) return; providerPending.current = true; setProvidersLoading(true); try { const response = await api.get("/email/providers"); if (!mounted.current) return; setProviders(response.data ?? []); setProviderStatusError(false); setProviderError(null); } catch (error) { if (!mounted.current) return; setProviders([]); setProviderStatusError(true); setProviderError(readError(error)); } finally { providerPending.current = false; if (mounted.current) { setProviderStatusLoaded(true); setProvidersLoading(false); } } }, []); useEffect(() => { void loadProviders(); }, [loadProviders]); const loadJobs = useCallback(async () => { const key = jobQuery.trim(); if (pendingJobs.current === key) return; const request = ++jobsRequest.current; pendingJobs.current = key; setJobsLoading(true); setJobsError(null); try { const response = await api.get("/jobapplications/choices", { params: { q: key || undefined, limit: 20 } }); if (!mounted.current || request !== jobsRequest.current) return; setJobs(Array.isArray(response.data) ? response.data : []); setLoadedJobQuery(key); setJobsLoaded(true); } catch (error) { if (!mounted.current || request !== jobsRequest.current) return; setJobsError(readError(error)); } finally { if (request === jobsRequest.current) { pendingJobs.current = null; if (mounted.current) setJobsLoading(false); } } }, [jobQuery]); useEffect(() => { const timer = window.setTimeout(() => void loadJobs(), jobQuery.trim() ? 250 : 0); return () => { window.clearTimeout(timer); jobsRequest.current += 1; pendingJobs.current = null; }; }, [jobQuery, loadJobs]); const loadStoredDrafts = useCallback(async (afterWrite = false) => { if (!mounted.current || (draftsPending.current && !afterWrite)) return; draftsPending.current = true; const request = ++draftsRequest.current; setDraftsLoading(true); try { const response = await api.get("/email/drafts"); if (mounted.current && request === draftsRequest.current) { setStoredDrafts(Array.isArray(response.data) ? response.data : []); setDraftsReadError(null); } } catch (error) { if (mounted.current && request === draftsRequest.current) { const failure = readError(error); setDraftsReadError(failure); if (failure.kind === "unauthorized") setStoredDrafts([]); } } finally { if (request === draftsRequest.current) { draftsPending.current = false; if (mounted.current) setDraftsLoading(false); } } }, []); useEffect(() => { void loadStoredDrafts(); }, [loadStoredDrafts]); const filteredSummary = useMemo(() => { if (!hasCurrentInbox) return { linked: 0, inbound: 0 }; const linked = items.filter((item) => item.externalThreadId).length; const inbound = items.filter((item) => item.direction === "inbound").length; return { linked, inbound }; }, [hasCurrentInbox, items]); const sendCapableProviders = useMemo(() => providers.filter((provider) => provider.connected && provider.canSend), [providers]); const loadMessage = async (item: CorrespondenceInboxItem) => { if (detailPending.current && detailItem.current?.id === item.id) return; const request = ++detailRequest.current; detailPending.current = true; detailItem.current = item; setSelectedMessageId(item.id); setMessageDetail(null); setDetailError(null); setDetailNotice(false); setDetailLoading(true); try { if (item.provider && item.provider !== "manual" && item.externalMessageId) { try { const live = await api.get("/email/message", { params: { provider: item.provider, messageId: item.externalMessageId }, }); if (request !== detailRequest.current) return; setMessageDetail(live.data); return; } catch { if (request !== detailRequest.current) return; setDetailNotice(true); } } const saved = await api.get(`/correspondence/message/${item.id}`); if (request !== detailRequest.current) return; setMessageDetail(saved.data); } catch (error) { if (request !== detailRequest.current) return; setDetailError(readError(error)); } finally { if (request === detailRequest.current) { detailPending.current = false; setDetailLoading(false); } } }; const showMessage = (item: CorrespondenceInboxItem) => { if (selectedMessageId !== item.id) return loadMessage(item); detailRequest.current += 1; detailItem.current = null; detailPending.current = false; setSelectedMessageId(null); setMessageDetail(null); setDetailLoading(false); }; const hasDraftContent = (current: EmailDraft | null) => !!current && !!(current.to.trim() || current.subject.trim() || current.bodyText.trim()); const draftDirty = !!draft && (!draftBaseline.current || draft.to !== draftBaseline.current.to || draft.subject !== draftBaseline.current.subject || draft.bodyText !== draftBaseline.current.bodyText); const jobsCurrent = loadedJobQuery === jobQuery.trim() && jobsError?.kind !== "unauthorized"; const draftErrorMessage = draftError ? (draftError.status === 409 ? "" : draftError.message) || t(draftError.action === "discard" ? draftError.status === 409 ? "emailInboxDraftChangedDiscard" : "emailInboxDraftDiscardFailed" : draftError.action === "prepare" ? draftError.status === 409 ? "emailInboxNewAttemptConflict" : "emailInboxNewAttemptFailed" : draftError.status === 409 ? "emailInboxDraftChangedElsewhere" : "emailInboxDraftSaveFailed") : ""; const openDraft = (next: EmailDraft | null) => { draftRef.current = next; draftBaseline.current = next; setDraft(next); }; const isCurrentDraft = (captured: EmailDraft) => mounted.current && draftRef.current?.clientRequestId === captured.clientRequestId && draftRef.current?.id === captured.id && draftRef.current?.jobApplicationId === captured.jobApplicationId && draftRef.current?.provider === captured.provider; const finishComposeOperation = () => { composeOperation.current = false; if (mounted.current) { setComposeBusy(false); setSavingDraft(false); setSending(false); } }; const replaceDraftApproved = () => { const current = draftRef.current; if (!hasDraftContent(current) && !sendResult) return null; const saved = !!current?.id && !draftDirty; const unresolved = sendResult && ["uncertain", "pending", "sending"].includes(sendResult.status); return confirm({ title: t("emailInboxReplaceDraftTitle"), message: (unresolved ? t("emailInboxDeliveryUncertain") + "\n\n" : "") + t(saved ? "emailInboxCurrentDraftSaved" : "emailInboxUnsavedDraftLost"), confirmLabel: t("emailInboxReplaceDraft"), destructive: !saved, }); }; const startReply = async (item: CorrespondenceInboxItem) => { const detail = messageDetail; const provider = providers.find((candidate) => candidate.provider === item.provider); if (!detail || !item.provider || !provider?.canSend || composeOperation.current) return; composeOperation.current = true; try { const approval = replaceDraftApproved(); if ((approval && !(await approval)) || !mounted.current) return; const subject = detail.subject.trim(); openDraft({ jobApplicationId: item.jobApplicationId, companyName: item.companyName || t("emailInboxUnknownCompany"), jobTitle: item.jobTitle || t("emailInboxUnknownRole"), provider: item.provider, providerName: provider.displayName, fromAddress: provider.address || t("emailInboxConnectedAccount"), to: item.direction === "outbound" ? detail.to : detail.from, subject: /^re:/i.test(subject) ? subject : `Re: ${subject || t("emailInboxYourMessage")}`, bodyText: "", threadId: detail.threadId || item.externalThreadId || "", clientRequestId: globalThis.crypto.randomUUID(), }); setSendResult(null); setSendError(null); setDraftError(null); } finally { finishComposeOperation(); } }; const resumeDraft = async (stored: StoredEmailDraft) => { if (composeOperation.current) return; composeOperation.current = true; try { const approval = replaceDraftApproved(); if ((approval && !(await approval)) || !mounted.current) return; const provider = providers.find((candidate) => candidate.provider === stored.provider); const item = items.find((candidate) => candidate.jobApplicationId === stored.jobApplicationId); openDraft({ ...stored, companyName: item?.companyName || "", jobTitle: item?.jobTitle || "", providerName: provider?.displayName || stored.provider, fromAddress: provider?.address || "", }); setSendResult(null); setSendError(null); setDraftError(null); } finally { finishComposeOperation(); } }; const openComposeSetup = () => { if (composeOperation.current) return; setComposeJobId((current) => current || jobs[0]?.id || ""); setComposeProvider((current) => current || sendCapableProviders[0]?.provider || ""); setComposeSetupOpen(true); }; const startNewMessage = async () => { const job = jobsCurrent ? jobs.find((candidate) => candidate.id === composeJobId) : undefined; const provider = sendCapableProviders.find((candidate) => candidate.provider === composeProvider); if (!job || !provider || jobsLoading || jobsError || composeOperation.current) return; composeOperation.current = true; try { const approval = replaceDraftApproved(); if ((approval && !(await approval)) || !mounted.current) return; openDraft({ jobApplicationId: job.id, companyName: job.companyName || t("emailInboxUnknownCompany"), jobTitle: job.jobTitle || t("emailInboxUnknownRole"), provider: provider.provider, providerName: provider.displayName, fromAddress: provider.address || t("emailInboxConnectedAccount"), to: "", subject: "", bodyText: "", threadId: "", clientRequestId: globalThis.crypto.randomUUID(), }); setComposeSetupOpen(false); setSendResult(null); setSendError(null); setDraftError(null); } finally { finishComposeOperation(); } }; const updateDraft = (changes: Partial>) => { const current = draftRef.current; if (!current || composeOperation.current || sendResult) return; const next = { ...current, ...changes }; draftRef.current = next; setDraft(next); setSendError(null); }; const discardDraft = async () => { const captured = draftRef.current; if (!captured || composeOperation.current) return; composeOperation.current = true; try { if ((captured.id || hasDraftContent(captured) || sendResult) && !(await confirm({ title: t("emailInboxDiscardDraftTitle"), message: (sendResult && ["uncertain", "pending", "sending"].includes(sendResult.status) ? t("emailInboxDeliveryUncertain") + "\n\n" : "") + t("emailInboxDiscardDraftMessage"), confirmLabel: t("emailInboxDiscardDraft"), destructive: true, }))) return; if (!isCurrentDraft(captured)) return; setComposeBusy(true); draftPanel.current?.focus({ preventScroll: true }); setDraftError(null); if (captured.id && captured.revision) await api.delete(`/email/drafts/${captured.id}`, { params: { revision: captured.revision } }); if (!isCurrentDraft(captured)) return; inboxPanel.current?.focus({ preventScroll: true }); openDraft(null); setSendResult(null); setSendError(null); if (captured.id) { setStoredDrafts((current) => current.filter((stored) => stored.id !== captured.id)); await loadStoredDrafts(true); } } catch (error) { if (isCurrentDraft(captured)) setDraftError({ ...readError(error), action: "discard" }); } finally { finishComposeOperation(); } }; const saveDraft = async () => { const captured = draftRef.current; if (!captured || composeOperation.current || sendResult) return; composeOperation.current = true; setComposeBusy(true); setSavingDraft(true); setDraftError(null); draftPanel.current?.focus({ preventScroll: true }); try { const response = captured.id && captured.revision ? await api.put(`/email/drafts/${captured.id}`, { revision: captured.revision, to: captured.to, subject: captured.subject, bodyText: captured.bodyText }) : await api.post("/email/drafts", { jobApplicationId: captured.jobApplicationId, provider: captured.provider, to: captured.to, subject: captured.subject, bodyText: captured.bodyText, threadId: captured.threadId || null }); if (!isCurrentDraft(captured)) return; openDraft({ ...captured, ...response.data }); setStoredDrafts((current) => [response.data, ...current.filter((stored) => stored.id !== response.data.id)]); await loadStoredDrafts(true); if (mounted.current) toast(latestT.current("emailInboxDraftSaved"), "success"); } catch (error) { if (isCurrentDraft(captured)) setDraftError({ ...readError(error), action: "save" }); } finally { finishComposeOperation(); } }; const sendDraft = async () => { const captured = draftRef.current; if (!captured || composeOperation.current || sendResult || !captured.to.trim() || !captured.subject.trim() || !captured.bodyText.trim()) return; composeOperation.current = true; try { if (!(await confirm({ title: t("emailInboxConfirmSendTitle"), message: t("emailInboxConfirmSendMessage", { provider: captured.providerName, from: captured.fromAddress, to: captured.to.trim(), subject: captured.subject.trim(), thread: captured.threadId || t("emailInboxNewMessage") }), confirmLabel: t("emailInboxSendEmail"), })) || !isCurrentDraft(captured)) return; setComposeBusy(true); setSending(true); setSendError(null); draftPanel.current?.focus({ preventScroll: true }); const response = await api.post("/email/send", { jobApplicationId: captured.jobApplicationId, provider: captured.provider, clientRequestId: captured.clientRequestId, to: captured.to.trim(), subject: captured.subject.trim(), bodyText: captured.bodyText, threadId: captured.threadId || null, confirmed: true, }); if (!isCurrentDraft(captured)) return; setSendResult(response.data); if (response.data.status === "sent") { toast(latestT.current("emailInboxSentSaved"), "success"); if (captured.id && captured.revision) { try { await api.delete(`/email/drafts/${captured.id}`, { params: { revision: captured.revision } }); if (!isCurrentDraft(captured)) return; openDraft({ ...captured, id: undefined, revision: undefined }); setStoredDrafts((current) => current.filter((stored) => stored.id !== captured.id)); } catch { if (!mounted.current) return; toast(latestT.current("emailInboxSentNewerDraft"), "warning"); } if (mounted.current) await loadStoredDrafts(true); } if (mounted.current) await latestLoad.current(); } } catch (error: any) { if (!isCurrentDraft(captured)) return; const result = error?.response?.data; if (["failed", "uncertain", "sent", "pending", "sending"].includes(result?.status)) setSendResult(result as EmailSendResult); else if (!error?.response || error.response.status >= 500 || error.response.status === 409) setSendResult({ attemptId: captured.clientRequestId, status: "uncertain", duplicate: false, failureCategory: "client_connection_lost" }); setSendError(readError(error)); } finally { finishComposeOperation(); } }; const prepareNewAttempt = async () => { const captured = draftRef.current; if (!captured || composeOperation.current || sendResult?.status !== "failed") return; composeOperation.current = true; setComposeBusy(true); setDraftError(null); draftPanel.current?.focus({ preventScroll: true }); try { if (captured.id && captured.revision) { const response = await api.post(`/email/drafts/${captured.id}/new-attempt`, { revision: captured.revision }); if (!isCurrentDraft(captured)) return; const stored = { ...captured, ...response.data }; openDraft({ ...stored, to: captured.to, subject: captured.subject, bodyText: captured.bodyText }); draftBaseline.current = stored; setStoredDrafts((current) => [response.data, ...current.filter((item) => item.id !== response.data.id)]); } else { if (!isCurrentDraft(captured)) return; const next = { ...captured, clientRequestId: globalThis.crypto.randomUUID() }; draftRef.current = next; setDraft(next); } setSendResult(null); setSendError(null); if (captured.id) await loadStoredDrafts(true); } catch (error) { if (isCurrentDraft(captured)) setDraftError({ ...readError(error), action: "prepare" }); } finally { finishComposeOperation(); } }; const unlinkGmailThread = async (item: CorrespondenceInboxItem) => { if (item.provider !== "gmail" || !item.externalThreadId || unlinkPending.current) return; unlinkPending.current = true; try { if (!(await confirm({ title: t("emailInboxUnlinkTitle"), message: t("emailInboxUnlinkMessage", { job: item.companyName || t("emailInboxThisJob") }), confirmLabel: t("emailInboxUnlinkThread"), destructive: true, })) || !mounted.current) return; setUnlinkingThreadId(item.externalThreadId); inboxPanel.current?.focus({ preventScroll: true }); setUnlinkError(null); await api.post("/gmail/unlink-thread", { jobApplicationId: item.jobApplicationId, threadId: item.externalThreadId, note: "Unlinked from Job email hub", nextDecision: "review", }); if (!mounted.current) return; if (selectedMessageId === item.id) { detailRequest.current += 1; setSelectedMessageId(null); setMessageDetail(null); } await latestLoad.current(); toast(t("emailInboxUnlinked"), "success"); } catch (error) { if (mounted.current) setUnlinkError({ ...readError(error), retryable: false }); } finally { unlinkPending.current = false; if (mounted.current) setUnlinkingThreadId(null); } }; const providerStatusLabel = (provider: EmailProviderStatus) => { if (!provider.connected) return t("emailInboxProviderNotConnected", { provider: provider.displayName }); const identity = provider.address || t("emailInboxConnectedAccount"); if (!provider.canRead) return t("emailInboxProviderReadUnavailable", { provider: provider.displayName, identity }); return t(provider.canSend ? "emailInboxProviderReadSend" : "emailInboxProviderReadOnly", { provider: provider.displayName, identity }); }; return ( {view === "inbox" && hasCurrentInbox ? } label={t(inboxTotal === 1 ? "emailInboxItem" : "emailInboxItems", { count: inboxTotal })} variant="outlined" /> : null} {view === "inbox" && hasCurrentInbox ? 0 ? "success" : "default"} /> : null} {view === "inbox" && hasCurrentInbox ? : null} {providers.map((provider) => ( ))} { providerPanel.current?.focus({ preventScroll: true }); return loadProviders(); }} /> { jobsPanel.current?.focus({ preventScroll: true }); return loadJobs(); }} /> {jobsLoaded && jobsCurrent && !jobsLoading && !jobsError && !jobQuery.trim() && jobs.length === 0 ? {t("emailInboxAddJobFirst")} : null} {providerStatusLoaded && !providerStatusError && sendCapableProviders.length === 0 ? {t("emailInboxReconnectToCompose")} : null} { draftsPanel.current?.focus({ preventScroll: true }); return loadStoredDrafts(); }} /> {storedDrafts.length > 0 ? ( {t(storedDrafts.length === 1 ? "emailInboxSavedDraftOne" : "emailInboxSavedDraftMany", { count: storedDrafts.length })} {storedDrafts.slice(0, 5).map((stored) => ( ))} ) : null} {composeSetupOpen ? ( {t("emailInboxCompose")} job.id === composeJobId) ?? null} onInputChange={(_, value, reason) => { if (reason === "input" || reason === "clear") setJobQuery(value); }} onChange={(_, value) => setComposeJobId(value?.id ?? "")} isOptionEqualToValue={(option, value) => option.id === value.id} getOptionLabel={(job) => `${job.companyName || t("emailInboxUnknownCompany")} · ${job.jobTitle}`} renderInput={(params) => } /> {t("emailInboxSendingProvider")} ) : null} {reviewVisited ? : null} ); }