diff --git a/CHANGELOG.md b/CHANGELOG.md index a71deba..560d709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.27 + +- Show a private Drafts destination in desktop and mobile navigation only when the signed-in user + has saved drafts. +- List saved drafts with mailbox and search filtering, and reopen the exact new-message, reply, or + forward draft with its attachments and conversation context. +- Keep the Drafts count synchronized when a composer creates, sends, or discards a draft. + ## 0.1.26 - Expand the OAuth-protected MCP server with conversation, thread, and attachment retrieval plus diff --git a/app/app.tsx b/app/app.tsx index b89e94a..2af26d5 100644 --- a/app/app.tsx +++ b/app/app.tsx @@ -5,6 +5,8 @@ import { Toaster } from "@/components/ui/sonner"; import { getCurrentUser } from "@/features/auth/api"; import { LoginPage } from "@/features/auth/login-page"; import type { CurrentUser } from "@/features/auth/types"; +import { DraftsPage } from "@/features/drafts/drafts-page"; +import { useDrafts } from "@/features/drafts/use-drafts"; import { InboxPage } from "@/features/inbox/inbox-page"; import { listMailboxes } from "@/features/mailboxes/api"; import type { Mailbox } from "@/features/mailboxes/types"; @@ -22,6 +24,11 @@ import { useAppRoute } from "@/lib/use-app-route"; const ComposeDialog = React.lazy(() => import("@/features/compose/compose-dialog").then((module) => ({ default: module.ComposeDialog })) ); +const DraftComposeDialog = React.lazy(() => + import("@/features/drafts/draft-compose-dialog").then((module) => ({ + default: module.DraftComposeDialog + })) +); export function App(): React.ReactElement { const [setup, setSetup] = React.useState(null); @@ -33,10 +40,17 @@ export function App(): React.ReactElement { const [composeOpen, setComposeOpen] = React.useState(false); const [isLoading, setIsLoading] = React.useState(true); const { navigate, route } = useAppRoute(setup?.isComplete); - const activeFolder: FolderId = route.kind === "settings" ? "settings" : route.folder; + const activeFolder: FolderId = + route.kind === "settings" ? "settings" : route.kind === "drafts" ? "drafts" : route.folder; const selectedId = route.kind === "mail" ? route.messageId : null; + const selectedDraftId = route.kind === "drafts" ? route.draftId : null; const settingsTab: SettingsTabId = route.kind === "settings" ? route.tab : "mailboxes"; const currentUserId = user?.id ?? null; + const draftState = useDrafts(currentUserId); + const selectedDraft = + selectedDraftId === null + ? null + : (draftState.drafts.find((draft) => draft.id === selectedDraftId) ?? null); const contentMailboxes = React.useMemo( () => mailboxes.filter((mailbox) => mailbox.accessLevel !== null), [mailboxes] @@ -121,7 +135,8 @@ export function App(): React.ReactElement { <> + ) : activeFolder === "drafts" ? ( + navigate({ kind: "drafts", draftId: null })} + onSelect={(draftId) => navigate({ kind: "drafts", draftId })} + /> ) : ( void draftState.refresh().catch(() => undefined)} onRefresh={() => void mailSync.refresh().catch(() => undefined)} onMessageRouteChange={(folder, messageId) => navigate({ kind: "mail", folder, messageId }) @@ -190,6 +218,20 @@ export function App(): React.ReactElement { mode="new" open={composeOpen} onOpenChange={setComposeOpen} + onDraftsChange={() => void draftState.refresh().catch(() => undefined)} + onSent={() => void mailSync.refresh().catch(() => undefined)} + /> + + ) : null} + {selectedDraft ? ( + + void draftState.refresh().catch(() => undefined)} + onOpenChange={(open) => { + if (!open) navigate({ kind: "drafts", draftId: null }); + }} onSent={() => void mailSync.refresh().catch(() => undefined)} /> diff --git a/app/components/layout/app-shell.tsx b/app/components/layout/app-shell.tsx index 1e1f1a9..de203a8 100644 --- a/app/components/layout/app-shell.tsx +++ b/app/components/layout/app-shell.tsx @@ -20,6 +20,7 @@ type AppShellProps = { updateInProgress: boolean; updateStatus: UpdateStatus | null; unread: UnreadCounts; + draftCount: number; onCompose: () => void; onFolderChange: (folder: FolderId) => void; onMailboxChange: (mailboxId: string) => void; @@ -33,6 +34,7 @@ export function AppShell(props: AppShellProps): React.ReactElement {
void; @@ -19,6 +20,7 @@ type MobileNavigationProps = { export function MobileNavigation({ activeFolder, + draftCount, unread, user, onFolderChange, @@ -64,6 +66,7 @@ export function MobileNavigation({ Navigation = { inbox: Inbox, sent: Send, + drafts: FilePenLine, starred: Star, archived: Archive, trash: Trash2, @@ -33,6 +44,7 @@ const icons: Record = { export function Sidebar({ activeFolder, + draftCount = 0, drawerAction, unread, user, @@ -41,6 +53,13 @@ export function Sidebar({ variant = "desktop" }: SidebarProps): React.ReactElement { const isDrawer = variant === "drawer"; + const navigationFolders: Array<(typeof mailFolders)[number] | typeof draftFolder> = []; + for (const folder of mailFolders) { + navigationFolders.push(folder); + if (folder.id === "sent" && (draftCount > 0 || activeFolder === "drafts")) { + navigationFolders.push(draftFolder); + } + } return (