mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: extend the message selection with shift+arrow keys #992
This commit is contained in:
@@ -683,6 +683,15 @@ export function EmailList({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedEmailId]);
|
||||
|
||||
// Keep the moving end of a Shift+Arrow range on screen (#992).
|
||||
const selectionFocusKey = useEmailStore((state) => state.selectionFocus?.focus ?? null);
|
||||
useEffect(() => {
|
||||
if (!selectionFocusKey) return;
|
||||
const index = threadGroups.findIndex((thread) => thread.emails.some((e) => emailKeyFor(e) === selectionFocusKey));
|
||||
if (index >= 0) virtualizer.scrollToIndex(index, { align: 'auto' });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectionFocusKey]);
|
||||
|
||||
// Re-measure all items when density or preview settings change
|
||||
useEffect(() => {
|
||||
virtualizer.measure();
|
||||
|
||||
@@ -1007,6 +1007,12 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
onDeselectAll: () => {
|
||||
clearSelection();
|
||||
},
|
||||
// Shift + ↓/↑ (and Shift + j/k): grow or shrink the ticked range from the
|
||||
// open message without opening each row on the way. (#992)
|
||||
onExtendSelection: (direction: 1 | -1) => {
|
||||
if (isScheduledView) return;
|
||||
useEmailStore.getState().extendSelection(direction);
|
||||
},
|
||||
// `x` under Mountain View: put the focused conversation in or out of the
|
||||
// selection. Only reachable there, where `x` is handed over from thread
|
||||
// expansion.
|
||||
|
||||
@@ -41,3 +41,52 @@ describe('useKeyboardShortcuts: x toggles thread expansion', () => {
|
||||
expect(onToggleThreadExpansion).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// #992: Shift + arrow (or j/k) extends the selection instead of moving to the
|
||||
// next message; plain arrows still move.
|
||||
describe('useKeyboardShortcuts: Shift + arrow extends the selection', () => {
|
||||
function press(key: string, code: string, shiftKey: boolean) {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key, code, shiftKey, bubbles: true }));
|
||||
}
|
||||
|
||||
it.each([
|
||||
['ArrowDown', 'ArrowDown', 1],
|
||||
['ArrowUp', 'ArrowUp', -1],
|
||||
['J', 'KeyJ', 1],
|
||||
['K', 'KeyK', -1],
|
||||
] as const)('Shift + %s (%s) extends by %i', (key, code, direction) => {
|
||||
const onExtendSelection = vi.fn();
|
||||
const onNextEmail = vi.fn();
|
||||
const onPreviousEmail = vi.fn();
|
||||
renderHook(() =>
|
||||
useKeyboardShortcuts({
|
||||
emails: [],
|
||||
selectedEmailId: 'e1',
|
||||
handlers: { onExtendSelection, onNextEmail, onPreviousEmail },
|
||||
}),
|
||||
);
|
||||
|
||||
press(key, code, true);
|
||||
|
||||
expect(onExtendSelection).toHaveBeenCalledWith(direction);
|
||||
expect(onNextEmail).not.toHaveBeenCalled();
|
||||
expect(onPreviousEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('plain ArrowDown still moves to the next message', () => {
|
||||
const onExtendSelection = vi.fn();
|
||||
const onNextEmail = vi.fn();
|
||||
renderHook(() =>
|
||||
useKeyboardShortcuts({
|
||||
emails: [],
|
||||
selectedEmailId: 'e1',
|
||||
handlers: { onExtendSelection, onNextEmail },
|
||||
}),
|
||||
);
|
||||
|
||||
press('ArrowDown', 'ArrowDown', false);
|
||||
|
||||
expect(onNextEmail).toHaveBeenCalledTimes(1);
|
||||
expect(onExtendSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface KeyboardShortcutHandlers {
|
||||
// Selection
|
||||
onSelectAll?: () => void;
|
||||
onDeselectAll?: () => void;
|
||||
/** Shift + ↓/j and Shift + ↑/k: grow or shrink the selection by one row. */
|
||||
onExtendSelection?: (direction: 1 | -1) => void;
|
||||
|
||||
// Thread actions
|
||||
onToggleThreadExpansion?: () => void;
|
||||
@@ -174,13 +176,23 @@ export function useKeyboardShortcuts({
|
||||
case "j":
|
||||
case "arrowdown":
|
||||
event.preventDefault();
|
||||
run("next-email", h.onNextEmail);
|
||||
if (event.shiftKey && h.onExtendSelection) {
|
||||
const extend = h.onExtendSelection;
|
||||
run("extend-selection-down", () => extend(1));
|
||||
} else {
|
||||
run("next-email", h.onNextEmail);
|
||||
}
|
||||
break;
|
||||
|
||||
case "k":
|
||||
case "arrowup":
|
||||
event.preventDefault();
|
||||
run("previous-email", h.onPreviousEmail);
|
||||
if (event.shiftKey && h.onExtendSelection) {
|
||||
const extend = h.onExtendSelection;
|
||||
run("extend-selection-up", () => extend(-1));
|
||||
} else {
|
||||
run("previous-email", h.onPreviousEmail);
|
||||
}
|
||||
break;
|
||||
|
||||
case "enter":
|
||||
@@ -396,6 +408,7 @@ export const KEYBOARD_SHORTCUTS = {
|
||||
{ key: "?", description: "shortcuts.global.help" },
|
||||
{ key: "Shift + G", description: "shortcuts.global.refresh" },
|
||||
{ key: "Ctrl + A", description: "shortcuts.global.select_all" },
|
||||
{ key: "Shift + ↓ / ↑", description: "shortcuts.global.extend_selection" },
|
||||
],
|
||||
threads: [
|
||||
{ key: "x", description: "shortcuts.threads.expand_collapse", layout: "default" },
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "إظهار الاختصارات",
|
||||
"refresh": "تحديث الرسائل",
|
||||
"select_all": "تحديد الكل",
|
||||
"global_search": "البحث في كل شيء"
|
||||
"global_search": "البحث في كل شيء",
|
||||
"extend_selection": "توسيع التحديد لأسفل / لأعلى"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "توسيع/طي المحادثة",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Mostra les dreceres",
|
||||
"refresh": "Actualitza els correus",
|
||||
"select_all": "Selecciona-ho tot",
|
||||
"global_search": "Cerca-ho tot"
|
||||
"global_search": "Cerca-ho tot",
|
||||
"extend_selection": "Amplia la selecció cap avall / amunt"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Desplega/replega el fil",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Zobrazit zkratky",
|
||||
"refresh": "Obnovit zprávy",
|
||||
"select_all": "Vybrat vše",
|
||||
"global_search": "Hledat ve všem"
|
||||
"global_search": "Hledat ve všem",
|
||||
"extend_selection": "Rozšířit výběr dolů / nahoru"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Rozbalit/sbalit vlákno",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Vis genveje",
|
||||
"refresh": "Opdatér e-mails",
|
||||
"select_all": "Vælg alle",
|
||||
"global_search": "Søg i alt"
|
||||
"global_search": "Søg i alt",
|
||||
"extend_selection": "Udvid markering ned / op"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Udvid/skjul tråd",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Tastaturkürzel anzeigen",
|
||||
"refresh": "E-Mails aktualisieren",
|
||||
"select_all": "Alle auswählen",
|
||||
"global_search": "Alles durchsuchen"
|
||||
"global_search": "Alles durchsuchen",
|
||||
"extend_selection": "Auswahl nach unten / oben erweitern"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Unterhaltung erweitern/einklappen",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Show shortcuts",
|
||||
"refresh": "Refresh emails",
|
||||
"select_all": "Select all",
|
||||
"global_search": "Search everything"
|
||||
"global_search": "Search everything",
|
||||
"extend_selection": "Extend selection down / up"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Expand/collapse thread",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Mostrar atajos",
|
||||
"refresh": "Actualizar correos",
|
||||
"select_all": "Seleccionar todo",
|
||||
"global_search": "Buscar en todo"
|
||||
"global_search": "Buscar en todo",
|
||||
"extend_selection": "Ampliar la selección hacia abajo / arriba"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Expandir/contraer conversación",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "نمایش میانبرها",
|
||||
"refresh": "بازنشانی ایمیلها",
|
||||
"select_all": "انتخاب همه",
|
||||
"global_search": "جستجو در همه جا"
|
||||
"global_search": "جستجو در همه جا",
|
||||
"extend_selection": "گسترش انتخاب به پایین / بالا"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "باز/بسته کردن رشته",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Afficher les raccourcis",
|
||||
"refresh": "Actualiser les emails",
|
||||
"select_all": "Tout sélectionner",
|
||||
"global_search": "Tout rechercher"
|
||||
"global_search": "Tout rechercher",
|
||||
"extend_selection": "Étendre la sélection vers le bas / le haut"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Développer/réduire la conversation",
|
||||
|
||||
@@ -2543,7 +2543,8 @@
|
||||
"help": "הצג קיצורי דרך",
|
||||
"refresh": "רענן מיילים",
|
||||
"select_all": "בחר הכל",
|
||||
"global_search": "חיפוש בכל מקום"
|
||||
"global_search": "חיפוש בכל מקום",
|
||||
"extend_selection": "הרחבת הבחירה למטה / למעלה"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "הרחב/כווץ את השרשור",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Parancsok mutatása",
|
||||
"refresh": "E-mailek frissítése",
|
||||
"select_all": "Összes kijelölése",
|
||||
"global_search": "Keresés mindenben"
|
||||
"global_search": "Keresés mindenben",
|
||||
"extend_selection": "Kijelölés bővítése lefelé / felfelé"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Beszélgetés kibontása/összecsukása",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Mostra scorciatoie",
|
||||
"refresh": "Aggiorna messaggi",
|
||||
"select_all": "Seleziona tutto",
|
||||
"global_search": "Cerca ovunque"
|
||||
"global_search": "Cerca ovunque",
|
||||
"extend_selection": "Estendi la selezione in basso / in alto"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Espandi/comprimi conversazione",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "ショートカットを表示",
|
||||
"refresh": "メールを更新",
|
||||
"select_all": "すべて選択",
|
||||
"global_search": "すべてを検索"
|
||||
"global_search": "すべてを検索",
|
||||
"extend_selection": "選択範囲を下 / 上に広げる"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "スレッドの展開/折りたたみ",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "단축키 보기",
|
||||
"refresh": "메일 새로고침",
|
||||
"select_all": "모두 선택",
|
||||
"global_search": "전체 검색"
|
||||
"global_search": "전체 검색",
|
||||
"extend_selection": "선택 범위를 아래 / 위로 확장"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "대화 펼치기/접기",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Rādīt īsinājumtaustiņus",
|
||||
"refresh": "Atsvaidzināt vēstules",
|
||||
"select_all": "Atlasīt visas",
|
||||
"global_search": "Meklēt visā"
|
||||
"global_search": "Meklēt visā",
|
||||
"extend_selection": "Paplašināt atlasi uz leju / uz augšu"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Izvērst/sairt sarunu",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Товчлол харуулах",
|
||||
"refresh": "Имэйл сэргээх",
|
||||
"select_all": "Бүгдийг сонго",
|
||||
"global_search": "Бүгдийг хайх"
|
||||
"global_search": "Бүгдийг хайх",
|
||||
"extend_selection": "Сонголтыг доош / дээш өргөтгөх"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Хэлхээсийг дэлгэх/хумих",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Vis tastatursnarveier",
|
||||
"refresh": "Oppdater e-poster",
|
||||
"select_all": "Velg alle",
|
||||
"global_search": "Søk i alt"
|
||||
"global_search": "Søk i alt",
|
||||
"extend_selection": "Utvid markeringen ned / opp"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Utvid eller fold sammen tråden",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Sneltoetsen tonen",
|
||||
"refresh": "E-mails vernieuwen",
|
||||
"select_all": "Alles selecteren",
|
||||
"global_search": "Alles doorzoeken"
|
||||
"global_search": "Alles doorzoeken",
|
||||
"extend_selection": "Selectie omlaag / omhoog uitbreiden"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Gesprek uitklappen/inklappen",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Pokaż skróty",
|
||||
"refresh": "Odśwież wiadomości",
|
||||
"select_all": "Zaznacz wszystko",
|
||||
"global_search": "Szukaj wszędzie"
|
||||
"global_search": "Szukaj wszędzie",
|
||||
"extend_selection": "Rozszerz zaznaczenie w dół / w górę"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Rozwiń/zwiń wątek",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Mostrar atalhos",
|
||||
"refresh": "Atualizar e-mails",
|
||||
"select_all": "Selecionar todos",
|
||||
"global_search": "Pesquisar em tudo"
|
||||
"global_search": "Pesquisar em tudo",
|
||||
"extend_selection": "Expandir a seleção para baixo / cima"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Expandir/recolher conversa",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Afișați comenzile rapide",
|
||||
"refresh": "Reîmprospătați e-mailurile",
|
||||
"select_all": "Selectează tot",
|
||||
"global_search": "Caută în tot"
|
||||
"global_search": "Caută în tot",
|
||||
"extend_selection": "Extinde selecția în jos / în sus"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Extindeți/reduceți firul de discuție",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Показать сочетания клавиш",
|
||||
"refresh": "Обновить письма",
|
||||
"select_all": "Выбрать все",
|
||||
"global_search": "Искать везде"
|
||||
"global_search": "Искать везде",
|
||||
"extend_selection": "Расширить выделение вниз / вверх"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Развернуть/свернуть цепочку",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Zobraziť skratky",
|
||||
"refresh": "Obnoviť e-maily",
|
||||
"select_all": "Vybrať všetko",
|
||||
"global_search": "Hľadať všade"
|
||||
"global_search": "Hľadať všade",
|
||||
"extend_selection": "Rozšíriť výber nadol / nahor"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Rozbaliť/zbaliť vlákno",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Kısayolları göster",
|
||||
"refresh": "E-postaları yenile",
|
||||
"select_all": "Tümünü seç",
|
||||
"global_search": "Her yerde ara"
|
||||
"global_search": "Her yerde ara",
|
||||
"extend_selection": "Seçimi aşağı / yukarı genişlet"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "İleti dizisini genişlet/daralt",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "Показати ярлики",
|
||||
"refresh": "Оновити електронні листи",
|
||||
"select_all": "Вибрати все",
|
||||
"global_search": "Шукати скрізь"
|
||||
"global_search": "Шукати скрізь",
|
||||
"extend_selection": "Розширити виділення вниз / вгору"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "Розгорнути/згорнути ланцюжок",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "顯示快速鍵",
|
||||
"refresh": "重新整理郵件",
|
||||
"select_all": "全部選取",
|
||||
"global_search": "全域搜尋"
|
||||
"global_search": "全域搜尋",
|
||||
"extend_selection": "向下 / 向上擴展選取"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "展開/收合對話串",
|
||||
|
||||
@@ -2625,7 +2625,8 @@
|
||||
"help": "显示快捷方式",
|
||||
"refresh": "刷新邮件",
|
||||
"select_all": "全选",
|
||||
"global_search": "全局搜索"
|
||||
"global_search": "全局搜索",
|
||||
"extend_selection": "向下 / 向上扩展选择"
|
||||
},
|
||||
"threads": {
|
||||
"expand_collapse": "展开/折叠会话",
|
||||
|
||||
@@ -146,6 +146,69 @@ describe('email-store selection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// #992: Shift+Arrow grows the selection from the open message one row at a
|
||||
// time and shrinks it again when the direction reverses.
|
||||
describe('extendSelection', () => {
|
||||
const keys = () => [...useEmailStore.getState().selectedEmailKeys].sort();
|
||||
|
||||
beforeEach(() => {
|
||||
// Opening a message makes it the anchor, as selectEmail does.
|
||||
useEmailStore.setState({ lastSelectedEmailKey: 'b', selectionFocus: null });
|
||||
});
|
||||
|
||||
it('selects the open message and the next one on the first press', () => {
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
expect(keys()).toEqual(['b', 'c']);
|
||||
});
|
||||
|
||||
it('keeps growing in the same direction', () => {
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
expect(keys()).toEqual(['b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it('shrinks back when the direction reverses', () => {
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
useEmailStore.getState().extendSelection(-1);
|
||||
expect(keys()).toEqual(['b', 'c']);
|
||||
});
|
||||
|
||||
it('crosses the anchor and grows the other way', () => {
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
useEmailStore.getState().extendSelection(-1);
|
||||
useEmailStore.getState().extendSelection(-1);
|
||||
expect(keys()).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('stops at the end of the list', () => {
|
||||
for (let i = 0; i < 10; i++) useEmailStore.getState().extendSelection(1);
|
||||
expect(keys()).toEqual(['b', 'c', 'd', 'e']);
|
||||
});
|
||||
|
||||
it('continues a shift-click range', () => {
|
||||
useEmailStore.getState().selectRangeEmails({ id: 'd' });
|
||||
useEmailStore.getState().extendSelection(-1);
|
||||
expect(keys()).toEqual(['b', 'c']);
|
||||
});
|
||||
|
||||
it('walks list rows, taking a whole thread with its row', () => {
|
||||
useEmailStore.setState({
|
||||
emails: ['t2', 'x', 't1', 'y'].map((id) => makeEmail(id, id.startsWith('t') ? 'thread-t' : `thread-${id}`)),
|
||||
listRows: [
|
||||
{ key: 'x', members: ['x'] },
|
||||
{ key: 't2', members: ['t2', 't1'] },
|
||||
{ key: 'y', members: ['y'] },
|
||||
],
|
||||
lastSelectedEmailKey: 'x',
|
||||
});
|
||||
useEmailStore.getState().extendSelection(1);
|
||||
expect(keys()).toEqual(['t1', 't2', 'x']);
|
||||
useEmailStore.getState().extendSelection(-1);
|
||||
expect(keys()).toEqual(['x']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAllEmails', () => {
|
||||
it('should select all emails', () => {
|
||||
useEmailStore.getState().selectAllEmails();
|
||||
|
||||
+54
-2
@@ -283,7 +283,17 @@ interface EmailStore {
|
||||
/** Take the email, not its id: the id alone collides across accounts. */
|
||||
toggleEmailSelection: (email: SelectableEmail) => void;
|
||||
selectRangeEmails: (target: SelectableEmail) => void;
|
||||
/**
|
||||
* Shift+Arrow (and Shift+J/K): grow or shrink the selection by one row,
|
||||
* from the open or last ticked message, like a desktop mail client. (#992)
|
||||
*/
|
||||
extendSelection: (direction: 1 | -1) => void;
|
||||
lastSelectedEmailKey: string | null;
|
||||
/**
|
||||
* The moving end of a keyboard range and the anchor it was taken from.
|
||||
* Only valid while the anchor is still lastSelectedEmailKey.
|
||||
*/
|
||||
selectionFocus: { anchor: string; focus: string } | null;
|
||||
/**
|
||||
* The message list's rows in screen order, each with the keys a click on it
|
||||
* selects. Shift-click ranges walk these, so a thread is in a range only
|
||||
@@ -2140,6 +2150,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
selectedEmailKeys: new Set(),
|
||||
retainedInViewIds: new Set(),
|
||||
lastSelectedEmailKey: null,
|
||||
selectionFocus: null,
|
||||
listRows: null,
|
||||
hasMoreEmails: false,
|
||||
totalEmails: 0,
|
||||
@@ -2411,7 +2422,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
for (let i = Math.min(anchorRow, targetRow); i <= Math.max(anchorRow, targetRow); i++) {
|
||||
for (const key of listRows[i].members) newSelection.add(key);
|
||||
}
|
||||
set({ selectedEmailKeys: newSelection });
|
||||
set({ selectedEmailKeys: newSelection, selectionFocus: { anchor: anchorKey, focus: targetKeyForRows } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2425,7 +2436,48 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
for (let i = start; i <= end; i++) {
|
||||
newSelection.add(emailKeyFor(emails[i]));
|
||||
}
|
||||
set({ selectedEmailKeys: newSelection });
|
||||
set({ selectedEmailKeys: newSelection, selectionFocus: { anchor: anchorKey, focus: targetKey } });
|
||||
},
|
||||
|
||||
extendSelection: (direction) => {
|
||||
const { emails, listRows, lastSelectedEmailKey, selectedEmail, selectedEmailKeys, selectionFocus } = get();
|
||||
// Walk the rows on screen, as shift-click does; without them, the emails.
|
||||
const rows: ListRow[] = listRows && listRows.length > 0
|
||||
? listRows
|
||||
: emails.map((e) => ({ key: emailKeyFor(e), members: [emailKeyFor(e)] }));
|
||||
if (rows.length === 0) return;
|
||||
const membersOf = (from: number, to: number) => {
|
||||
const keys: string[] = [];
|
||||
for (let i = Math.min(from, to); i <= Math.max(from, to); i++) keys.push(...rows[i].members);
|
||||
return keys;
|
||||
};
|
||||
|
||||
const anchorKey = lastSelectedEmailKey ?? (selectedEmail ? emailKeyFor(selectedEmail) : null);
|
||||
const anchor = anchorKey ? findListRow(rows, anchorKey) : -1;
|
||||
if (!anchorKey || anchor === -1) {
|
||||
// Nothing to extend from: start with the row at the end the key points from.
|
||||
const start = rows[direction > 0 ? 0 : rows.length - 1];
|
||||
set({
|
||||
selectedEmailKeys: new Set([...selectedEmailKeys, ...start.members]),
|
||||
lastSelectedEmailKey: start.key,
|
||||
selectionFocus: { anchor: start.key, focus: start.key },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Continue the range this anchor already has, or begin one at the anchor.
|
||||
const previous = selectionFocus && selectionFocus.anchor === anchorKey && selectedEmailKeys.size > 0
|
||||
? findListRow(rows, selectionFocus.focus)
|
||||
: -1;
|
||||
const focus = previous === -1 ? anchor : previous;
|
||||
const nextFocus = Math.max(0, Math.min(rows.length - 1, focus + direction));
|
||||
|
||||
const newSelection = new Set(selectedEmailKeys);
|
||||
if (previous !== -1) {
|
||||
for (const key of membersOf(anchor, previous)) newSelection.delete(key);
|
||||
}
|
||||
for (const key of membersOf(anchor, nextFocus)) newSelection.add(key);
|
||||
set({ selectedEmailKeys: newSelection, selectionFocus: { anchor: anchorKey, focus: rows[nextFocus].key } });
|
||||
},
|
||||
|
||||
selectAllEmails: () => {
|
||||
|
||||
Reference in New Issue
Block a user