mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
fix: keep the reader still while the list changes, and give Pro tabs no list ticks
This commit is contained in:
@@ -63,4 +63,24 @@ describe('the reader panel prop listSelection', () => {
|
||||
act(() => { useEmailStore.setState({ emails: [row('new'), row('a'), row('b'), row('c')] }); });
|
||||
expect(seen.at(-1)?.listSelection).toBe(before);
|
||||
});
|
||||
|
||||
it('does not render the reader again when the list changes but the ticks do not', () => {
|
||||
useAuthStore.setState({ client });
|
||||
useEmailStore.setState({ emails: [row('a'), row('b')], selectedEmailKeys: new Set(['a', 'b']), selectedMailbox: 'inbox', mailboxes });
|
||||
render(<EmailViewer email={null} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} />);
|
||||
const renders = seen.length;
|
||||
act(() => { useEmailStore.setState({ emails: [row('new'), row('a'), row('b')] }); });
|
||||
act(() => { useEmailStore.setState({ emails: [row('newer'), row('new'), row('a'), row('b')], threadEmailsCache: new Map() }); });
|
||||
expect(seen.length).toBe(renders);
|
||||
// A new tick does.
|
||||
act(() => { useEmailStore.setState({ selectedEmailKeys: new Set(['a', 'b', 'new']) }); });
|
||||
expect(seen.length).toBeGreaterThan(renders);
|
||||
});
|
||||
|
||||
it('gives a panel outside the main list (a Pro tab) no ticks', () => {
|
||||
useAuthStore.setState({ client });
|
||||
useEmailStore.setState({ emails: [row('a'), row('b')], selectedEmailKeys: new Set(['a', 'b']), selectedMailbox: 'inbox', mailboxes });
|
||||
render(<EmailViewer email={null} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} withListSelection={false} />);
|
||||
expect(seen.at(-1)).toMatchObject({ listSelection: null });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -288,6 +288,8 @@ interface EmailViewerProps {
|
||||
mailboxes?: Mailbox[];
|
||||
selectedMailbox?: string;
|
||||
className?: string;
|
||||
/** The side panel hears of the messages ticked in the main list (false: a Pro tab, whose message isn't from that list). */
|
||||
withListSelection?: boolean;
|
||||
}
|
||||
|
||||
// Helper function to format recipients with contextual display
|
||||
@@ -1313,6 +1315,7 @@ export function EmailViewer({
|
||||
mailboxes = [],
|
||||
selectedMailbox = "",
|
||||
className,
|
||||
withListSelection = true,
|
||||
}: EmailViewerProps) {
|
||||
const t = useTranslations('email_viewer');
|
||||
const archiveActionEnabled = useArchiveActionEnabled();
|
||||
@@ -1819,7 +1822,7 @@ export function EmailViewer({
|
||||
}, []);
|
||||
const [detailFocusRequest, setDetailFocusRequest] = useState(0);
|
||||
// The messages ticked in the list, for the AI assistant (lib/plugin-sandbox/assistant).
|
||||
const listSelection = useListSelection();
|
||||
const listSelection = useListSelection(withListSelection);
|
||||
const pluginDetailProps = useMemo(
|
||||
() => ({ email: pluginEmailView, selection: bodySelection, listSelection, focusRequest: detailFocusRequest }),
|
||||
[pluginEmailView, bodySelection, listSelection, detailFocusRequest],
|
||||
|
||||
@@ -143,6 +143,12 @@ describe('Pro email view - editing a draft', () => {
|
||||
expect(initialData.draftAccountId).toBeUndefined();
|
||||
});
|
||||
|
||||
// The ticks in the main list are another view's: the tab's side panel doesn't get them.
|
||||
it('gives the side panel of an email tab no list selection', async () => {
|
||||
await openAndEdit(undefined);
|
||||
expect(viewerProps.current?.withListSelection).toBe(false);
|
||||
});
|
||||
|
||||
// Pro shell with the 0.16.23 login active and the 1.0 login's Drafts
|
||||
// browsed: the store reads a row of that list through the 1.0 login. A
|
||||
// view that names no login can't tell, so it names none - the active one
|
||||
|
||||
@@ -541,6 +541,7 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId,
|
||||
currentMailboxRole={currentMailboxRole}
|
||||
mailboxes={mailboxes}
|
||||
className="flex-1"
|
||||
withListSelection={false}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ConfirmDialog {...confirmDialogProps} />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// the reader panel's `listSelection` prop, so the assistant can work on
|
||||
// several at once ("summarise these").
|
||||
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
||||
import { resolveEmailBlobContext, resolveSelectedEmails, useEmailStore } from '@/stores/email-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
@@ -33,18 +33,15 @@ export function listSelectionOf(state: SelectionState, client: IJMAPClient | nul
|
||||
return { emails, total };
|
||||
}
|
||||
|
||||
/** listSelectionOf for the list on screen; the same object while the selection stays the same. */
|
||||
export function useListSelection(): ListSelectionView | null {
|
||||
const selectedEmailKeys = useEmailStore((s) => s.selectedEmailKeys);
|
||||
const emails = useEmailStore((s) => s.emails);
|
||||
const threadEmailsCache = useEmailStore((s) => s.threadEmailsCache);
|
||||
/**
|
||||
* listSelectionOf for the list on screen; the same object while the
|
||||
* selection stays the same. The store is read through one selector that
|
||||
* answers a string, so new mail, a search or a moved message re-render the
|
||||
* caller only when the ticked messages change. `enabled` false (a Pro tab,
|
||||
* whose panel isn't about the main list) always answers null.
|
||||
*/
|
||||
export function useListSelection(enabled = true): ListSelectionView | null {
|
||||
const client = useAuthStore((s) => s.client);
|
||||
const last = useRef<{ key: string; value: ListSelectionView | null }>({ key: 'null', value: null });
|
||||
return useMemo(() => {
|
||||
const value = listSelectionOf({ selectedEmailKeys, emails, threadEmailsCache }, client);
|
||||
const key = JSON.stringify(value);
|
||||
// New mail arriving re-renders the list; the panel only hears of a new selection.
|
||||
if (key !== last.current.key) last.current = { key, value };
|
||||
return last.current.value;
|
||||
}, [selectedEmailKeys, emails, threadEmailsCache, client]);
|
||||
const key = useEmailStore((s) => (enabled ? JSON.stringify(listSelectionOf(s, client)) : 'null'));
|
||||
return useMemo(() => JSON.parse(key) as ListSelectionView | null, [key]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user