fix: stop calling a folder empty while a filter chip or category tab hides its mail

This commit is contained in:
Linus Rath
2026-11-03 05:40:49 +01:00
parent a529d09860
commit 7406cc1362
3 changed files with 23 additions and 2 deletions
@@ -71,6 +71,14 @@ describe('plugin list filters in the message list', () => {
expect(fetchEmails).toHaveBeenCalledTimes(2);
});
it('does not call the folder empty while a filter hides its mail', async () => {
render(<EmailList emails={[]} />);
expect(await screen.findByText('no_emails_description')).toBeInTheDocument();
act(() => { fireEvent.click(screen.getByRole('button', { name: 'Open' })); });
expect(screen.getByText('no_emails')).toBeInTheDocument();
expect(screen.queryByText('no_emails_description')).toBeNull();
});
it.each([
['a search', { searchQuery: 'invoice' }],
['a tag view', { selectedKeyword: 'work' }],
+5 -2
View File
@@ -48,7 +48,7 @@ import { getOwnAddresses } from "@/lib/filters/quick-rule-target";
import { emitThreadExpand } from "@/lib/plugin-thread-expand";
import { normalizeAddress } from "@/lib/filters/quick-rules";
import { PluginSlot } from "@/components/plugins/plugin-slot";
import { PluginListFilters } from "./plugin-list-filters";
import { PluginListFilters, useFolderListFiltered } from "./plugin-list-filters";
import { PluginActions, useFolderAccountKind } from "@/components/plugins/plugin-actions";
import { useUnreadFirstSection } from "@/hooks/use-unread-first-section";
import { useTimeoutWhileMounted } from "@/hooks/use-timeout-while-mounted";
@@ -532,6 +532,9 @@ export function EmailList({
);
const bulkActionContext = useMemo(() => ({ emailIds: selectedEmailIds }), [selectedEmailIds]);
const folderAccountKind = useFolderAccountKind();
// A filter chip or a category tab on: the folder itself may well have mail.
const pluginFilterOn = useFolderListFiltered(effectiveMailboxRole ?? null, folderAccountKind);
const listNarrowed = pluginFilterOn || useMessageListTabsStore.getState().getCategoryFilter(effectiveMailboxRole) !== null;
// "Remove from this folder" (#615): offered for a selection only when every
// selected message is also in another folder.
const removeFromFolder = useRemoveFromFolder();
@@ -1222,7 +1225,7 @@ export function EmailList({
<EmptyState
icon={isScheduledView ? CalendarClock : isSearching ? SearchX : MailX}
title={isScheduledView ? t('no_scheduled_emails') : isSearching ? t('no_search_results') : t('no_emails')}
description={isScheduledView ? t('no_scheduled_emails_description') : isSearching ? t('no_search_results_description') : t('no_emails_description')}
description={isScheduledView ? t('no_scheduled_emails_description') : isSearching ? t('no_search_results_description') : listNarrowed ? undefined : t('no_emails_description')}
actions={!isScheduledView && isSearching && onClearSearch ? (
<Button size="sm" variant="outline" onClick={onClearSearch}>{tSidebar('clear_search')}</Button>
) : !isScheduledView && !isSearching && effectiveMailboxRole === 'drafts' && onCompose ? (
+10
View File
@@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import { iconForName, X } from "@/components/icons";
import { cn } from "@/lib/utils";
import {
activeListFilterCondition,
activeListFilterKeys,
listFilterApplies,
listFilterRevision,
@@ -16,6 +17,15 @@ import type { MailAccountKind } from "@/lib/plugin-sandbox/account-kind";
const NONE = new Set<string>();
/** Whether a plugin's filter is narrowing this folder's list right now. */
export function useFolderListFiltered(mailboxRole: string | null, accountKind: MailAccountKind | null): boolean {
return useSyncExternalStore(
subscribeListFilters,
() => activeListFilterCondition({ role: mailboxRole, accountKind }) !== null,
() => false,
);
}
/**
* The keyword filters plugins offer for this folder (api.ui.setListFilters),
* as chips in the list header. A chip turns its filter on; an active chip