import type { Folder } from '@nylas-labs/cli-kit/v3' import { useQuery } from '@tanstack/react-query' import { createFileRoute, Link, Outlet, useNavigate, useRouterState } from '@tanstack/react-router' import { Menu, Pencil } from 'lucide-react' import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { AppRailLogo, AppRailMobileNav, AppRailNav, type MailboxAccountOption } from '#app/components/AppRail' import { CommandPalette, useCommandPaletteShortcut } from '#app/components/CommandPalette' import { MobileTabBar } from '#app/components/MobileTabBar' import { CHROME_ROW_CLASS, CHROME_ROW_SHELL_CLASS, MAIL_HEADER_GRID_CLASS, MAIL_SIDEBAR_WIDTH_CLASS, } from '#app/config/layout' import { mailboxInfoQueryOptions } from '#app/query/mailbox-info' import { MailSearchBar } from '#features/mail/components/MailSearchBar' import { MailSidebar } from '#features/mail/components/MailSidebar' import { activeMailSidebarFolderId, composeSearchFromMailLocation, liveSearchTarget, mailSearchInputValue, } from '#features/mail/lib/mail-ui-model' import { foldersQueryOptions } from '#features/mail/state/mail-queries' import { getFolders } from '#server/fns' import { Sheet } from '#shared/components/Sheet' import { cn } from '#shared/lib/utils' export const Route = createFileRoute('/mail')({ loader: async ({ context }) => { const [info, folders] = await Promise.all([ context.queryClient.ensureQueryData(mailboxInfoQueryOptions()), context.queryClient.ensureQueryData(foldersQueryOptions(() => getFolders())), ]) return { info, folders } }, staleTime: Number.POSITIVE_INFINITY, component: MailLayout, }) type MailInfo = { email: string displayName?: string appName: string accounts?: MailboxAccountOption[] } function MailLayout() { const { info: initialInfo, folders: initialFolders } = Route.useLoaderData() const { data: info } = useQuery({ ...mailboxInfoQueryOptions(), initialData: initialInfo, initialDataUpdatedAt: 0, }) const { data: folders } = useQuery({ ...foldersQueryOptions(() => getFolders()), initialData: initialFolders, }) return } export function MailRouteScreen({ info, folders, defaultFolderId, children, }: { info: MailInfo folders: Folder[] defaultFolderId?: string children?: ReactNode }) { const navigate = useNavigate() const pathname = useRouterState({ select: (state) => state.location.pathname }) const isSearchRoute = useRouterState({ select: (state) => state.matches.some((match) => match.routeId === '/mail/search'), }) const searchParams = useRouterState({ select: (state) => state.location.search as Record }) const searchScopeFolderId = typeof searchParams.folderId === 'string' ? searchParams.folderId : undefined const routeSearchQuery = typeof searchParams.q === 'string' ? searchParams.q : undefined const currentFolderId = useMemo( () => activeMailSidebarFolderId(pathname, searchScopeFolderId) ?? defaultFolderId, [defaultFolderId, pathname, searchScopeFolderId], ) const selectedSearchThreadId = typeof searchParams.threadId === 'string' ? searchParams.threadId : undefined const labelBaseFolder = typeof searchParams.baseFolderId === 'string' ? searchParams.baseFolderId : undefined const activeSearchFolderId = currentFolderId ?? searchScopeFolderId const composeSearch = useMemo( () => composeSearchFromMailLocation(pathname, activeSearchFolderId, selectedSearchThreadId), [activeSearchFolderId, pathname, selectedSearchThreadId], ) const searchAwarePathname = isSearchRoute ? '/mail/search' : pathname const [query, setQuery] = useState('') const [sidebarOpen, setSidebarOpen] = useState(false) const [paletteOpen, setPaletteOpen] = useState(false) const openPalette = useCallback(() => setPaletteOpen(true), []) const closePalette = useCallback(() => setPaletteOpen(false), []) const focusSearch = useCallback(() => { closePalette() document.getElementById('mail-search')?.focus() }, [closePalette]) useCommandPaletteShortcut(openPalette) useEffect(() => { setQuery(mailSearchInputValue(searchAwarePathname, routeSearchQuery)) }, [routeSearchQuery, searchAwarePathname]) async function navigateSearch(nextQuery: string) { const target = liveSearchTarget( nextQuery, searchAwarePathname, activeSearchFolderId, selectedSearchThreadId, ) if (target.kind === 'search') { await navigate({ to: '/mail/search', search: { q: target.q, ...(target.folderId ? { folderId: target.folderId } : {}) }, replace: true, }) } else if (target.kind === 'thread') { await navigate({ to: '/mail/f/$folderId/t/$threadId', params: { folderId: target.folderId, threadId: target.threadId }, replace: true, }) } else if (target.kind === 'folder') { await navigate({ to: '/mail/f/$folderId', params: { folderId: target.folderId }, replace: true, }) } } useEffect(() => { function onKeyDown(event: KeyboardEvent) { const target = event.target as HTMLElement | null const isTyping = target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA' || target?.isContentEditable if (isTyping || event.repeat || event.metaKey || event.ctrlKey || event.altKey) return if (event.key === '/') { event.preventDefault() document.getElementById('mail-search')?.focus() } if (event.key.toLowerCase() === 'c') { event.preventDefault() navigate({ to: '/mail/compose', search: composeSearch, }) } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [composeSearch, navigate]) const sidebarProps = { folders, composeSearch, currentFolderId, baseFolderId: labelBaseFolder, onNavigate: () => setSidebarOpen(false), onFolderDeleted: (folderId: string) => { if (currentFolderId === folderId) { navigate({ to: '/mail/f/$folderId', params: { folderId: 'inbox' } }) } }, } const railNavProps = { email: info.email, displayName: info.displayName, accounts: info.accounts, active: 'mail' as const, onOpenCommandPalette: openPalette, } return (
{children ?? }
setSidebarOpen(false)} title="Navigation"> setSidebarOpen(false)} showDestinations={false} />
) }