diff --git a/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx b/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx index 5e83815b..faf30bc5 100644 --- a/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Email/EmailList.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Link, useNavigate } from 'react-router'; +import { Link, useNavigate, useSearchParams } from 'react-router'; import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query'; import { ChevronLeft, @@ -19,7 +19,6 @@ import { import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; -import { useGlobal } from 'hooks/useGlobal'; import type { EmailSummary } from 'types'; import { useComposer } from './Compose'; import { emailPath, useSelectedEmailId } from './shared'; @@ -57,7 +56,13 @@ export const EmailList = () => { const [, openCompose] = useComposer(); const navigate = useNavigate(); const selectedId = useSelectedEmailId(); - const [folder, setFolder] = useGlobal('EMAIL_FOLDER', 'inbox'); + // Which folder you are reading is addressable state, so it is a query param rather than a global. + // It stays a *param* and not a path segment because the path already spells which email is open: + // `/email/:emailId` is the thing, the folder is the view you found it through, and switching folders + // with a message open should not close it. + const [searchParams, setSearchParams] = useSearchParams(); + const folder = searchParams.get('folder') ?? 'inbox'; + const qs = searchParams.toString() ? `?${searchParams}` : ''; const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); @@ -66,9 +71,11 @@ export const EmailList = () => { const t = setTimeout(() => setDebouncedSearch(search.trim()), 300); return () => clearTimeout(t); }, [search]); + // Page 1 on a new search *or* a new folder. The folder half used to live in the click handler; it + // cannot any more, because the folder pills are links and nobody handles their click. useEffect(() => { setPage(1); - }, [debouncedSearch]); + }, [debouncedSearch, folder]); const { data: emailAccounts = [], refetch: refetchAccounts } = useQuery({ queryKey: ['email-accounts'], @@ -102,9 +109,18 @@ export const EmailList = () => { useEffect(() => { if (!isSearching && folder === 'inbox' && data?.total === 0 && allCount && allCount.total > 0) { - setFolder('all'); + // `replace`: this is the app correcting its own default, not a place you chose to be, so Back + // should leave /email rather than bounce you between inbox and all. + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set('folder', 'all'); + return next; + }, + { replace: true }, + ); } - }, [isSearching, folder, data?.total, allCount]); + }, [isSearching, folder, data?.total, allCount, setSearchParams]); // Live updates: while /email is open, listen for new-mail pushes (IMAP IDLE → SSE) and refetch. // The EventSource closes automatically when this component unmounts (i.e. when you leave /email). @@ -126,11 +142,6 @@ export const EmailList = () => { return () => es.close(); }, [queryClient]); - const handleFolderChange = (newFolder: string) => { - setFolder(newFolder); - setPage(1); - }; - const [syncing, setSyncing] = useState(false); const handleSync = async () => { @@ -191,13 +202,13 @@ export const EmailList = () => { // `replace` on purpose: sweeping down a folder with the arrow keys would otherwise stack one // history entry per row, and Back would then walk the sweep instead of leaving the mailbox. // A click is a real link and does push. - navigate(emailPath(nextId), { replace: true }); + navigate(emailPath(nextId, qs), { replace: true }); document.querySelector(`[data-email-id="${nextId}"]`)?.scrollIntoView({ block: 'nearest' }); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [messages, selectedId, navigate]); + }, [messages, selectedId, navigate, qs]); if (isLoading) { return
Loading emails...
; @@ -247,18 +258,24 @@ export const EmailList = () => { {FOLDERS.map((f) => { const Icon = f.icon; const isActive = folder === f.key; + // Search-only `to`, so react-router keeps the current pathname — the open email survives a + // folder switch. Built from the live params rather than written flat, so anything else in + // the query string comes along. + const next = new URLSearchParams(searchParams); + next.set('folder', f.key); return ( - + ); })} @@ -340,7 +357,7 @@ export const EmailList = () => { return ( `/email/${encodeURIComponent(id)}`; +/** `search` carries the folder you are reading in (`?folder=sent`), which a bare pathname would drop. */ +export const emailPath = (id: string, search = '') => `/email/${encodeURIComponent(id)}${search}`; /** The open email, or null on the bare `/email` route — which is a real state, not one to redirect away. */ export const useSelectedEmailId = (): string | null => useParams<{ emailId?: string }>().emailId ?? null;