@remit/web-client 0.0.48 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/components/mail/BriefPane.tsx +149 -3
- package/src/components/mail/DailyBrief.tsx +44 -12
- package/src/components/mail/FlaggedList.tsx +51 -27
- package/src/components/mail/FlaggedPane.tsx +144 -3
- package/src/components/mail/MailboxPane.tsx +45 -143
- package/src/components/mail/MessageRow.tsx +20 -8
- package/src/components/mail/ThreadListInteraction.test.ts +199 -0
- package/src/components/mail/ThreadListInteraction.tsx +408 -0
- package/src/hooks/useDeleteMessages.ts +16 -4
- package/src/hooks/useListCursor.test.ts +273 -0
- package/src/hooks/useListCursor.ts +226 -0
- package/src/hooks/useMarkAsRead.ts +15 -3
- package/src/hooks/useThreadActions.ts +101 -0
- package/src/hooks/useThreadMessageIds.ts +33 -0
- package/src/hooks/useTriageLayer.ts +148 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pane-level interaction layer: the bridge between a mounted list and the
|
|
3
|
+
* global keyboard dispatcher, plus next/previous adjacency.
|
|
4
|
+
*
|
|
5
|
+
* The list publishes `MessageListCommands` into `listCommandsRef` and reports
|
|
6
|
+
* its cursor and selection through `onTriageContextChange`; the keyboard hook
|
|
7
|
+
* registers the navigation and selection keys only while a list is serving
|
|
8
|
+
* them, so with no list mounted Enter, Space and ⌘A stay with the browser.
|
|
9
|
+
*
|
|
10
|
+
* This lived inside `MailboxPane`, which is why the brief and Flagged had a
|
|
11
|
+
* keyboard hint bar and no keyboard (#149). Every pane mounts it now.
|
|
12
|
+
*
|
|
13
|
+
* It is two hooks because a pane's verbs are aimed at the focused row: the
|
|
14
|
+
* context comes first, the pane builds its handlers from it, and the keyboard
|
|
15
|
+
* registration comes last.
|
|
16
|
+
*/
|
|
17
|
+
import { type RefObject, useCallback, useRef, useState } from "react";
|
|
18
|
+
import type { MessageListCommands } from "@/components/mail/MessageList";
|
|
19
|
+
import {
|
|
20
|
+
type TriageHandlers,
|
|
21
|
+
useTriageKeyboard,
|
|
22
|
+
} from "@/hooks/useTriageKeyboard";
|
|
23
|
+
import { adjacentMessageId } from "@/lib/adjacent-message";
|
|
24
|
+
|
|
25
|
+
export interface TriageContextUpdate {
|
|
26
|
+
focusedMessageId: string | undefined;
|
|
27
|
+
selectedIds: string[];
|
|
28
|
+
hasList: boolean;
|
|
29
|
+
blocksKeyboard: boolean;
|
|
30
|
+
/** Row ids in display order, when the list knows them. Feeds adjacency. */
|
|
31
|
+
orderedIds?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TriageContext {
|
|
35
|
+
listCommandsRef: RefObject<MessageListCommands | null>;
|
|
36
|
+
onTriageContextChange: (context: TriageContextUpdate) => void;
|
|
37
|
+
/** The list's roving cursor — where a verb with no selection is aimed. */
|
|
38
|
+
focusedMessageId: string | undefined;
|
|
39
|
+
/** The list's checkbox set — what a verb targets when it is non-empty. */
|
|
40
|
+
selectedIds: string[];
|
|
41
|
+
/** Row ids in display order, as last reported by the list. */
|
|
42
|
+
orderedIds: string[];
|
|
43
|
+
hasList: boolean;
|
|
44
|
+
blocksKeyboard: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const useTriageContext = (): TriageContext => {
|
|
48
|
+
const [focusedMessageId, setFocusedMessageId] = useState<string | undefined>(
|
|
49
|
+
undefined,
|
|
50
|
+
);
|
|
51
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
52
|
+
const [orderedIds, setOrderedIds] = useState<string[]>([]);
|
|
53
|
+
// Null whenever no list is mounted (reading-only phone view, drafts) — the
|
|
54
|
+
// keyboard layer then simply has nothing to drive.
|
|
55
|
+
const listCommandsRef = useRef<MessageListCommands | null>(null);
|
|
56
|
+
const [hasList, setHasList] = useState(false);
|
|
57
|
+
const [blocksKeyboard, setBlocksKeyboard] = useState(false);
|
|
58
|
+
|
|
59
|
+
const onTriageContextChange = useCallback((context: TriageContextUpdate) => {
|
|
60
|
+
setFocusedMessageId(context.focusedMessageId);
|
|
61
|
+
setSelectedIds(context.selectedIds);
|
|
62
|
+
if (context.orderedIds) setOrderedIds(context.orderedIds);
|
|
63
|
+
setHasList(context.hasList);
|
|
64
|
+
setBlocksKeyboard(context.blocksKeyboard);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
listCommandsRef,
|
|
69
|
+
onTriageContextChange,
|
|
70
|
+
focusedMessageId,
|
|
71
|
+
selectedIds,
|
|
72
|
+
orderedIds,
|
|
73
|
+
hasList,
|
|
74
|
+
blocksKeyboard,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface UseTriageLayerOptions {
|
|
79
|
+
context: TriageContext;
|
|
80
|
+
/** Message ids in display order — the adjacency source for next/previous. */
|
|
81
|
+
orderedIds: string[];
|
|
82
|
+
selectedMessageId: string | undefined;
|
|
83
|
+
/** Suspend the whole layer (e.g. a compose surface owns the keyboard). */
|
|
84
|
+
enabled?: boolean;
|
|
85
|
+
/** Close the open thread. Runs only when there is no selection to unwind. */
|
|
86
|
+
onClose: () => void;
|
|
87
|
+
/** The pane's own verbs — reply, star, delete and the rest. */
|
|
88
|
+
handlers: TriageHandlers;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TriageLayer {
|
|
92
|
+
/** Esc: unwinds the selection first, then the open thread. */
|
|
93
|
+
goBack: () => void;
|
|
94
|
+
nextMessageId: string | undefined;
|
|
95
|
+
previousMessageId: string | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const useTriageLayer = ({
|
|
99
|
+
context,
|
|
100
|
+
orderedIds,
|
|
101
|
+
selectedMessageId,
|
|
102
|
+
enabled = true,
|
|
103
|
+
onClose,
|
|
104
|
+
handlers,
|
|
105
|
+
}: UseTriageLayerOptions): TriageLayer => {
|
|
106
|
+
const { listCommandsRef, hasList, blocksKeyboard } = context;
|
|
107
|
+
|
|
108
|
+
const goBack = useCallback(() => {
|
|
109
|
+
if (listCommandsRef.current?.clearSelection()) return;
|
|
110
|
+
if (selectedMessageId) onClose();
|
|
111
|
+
}, [listCommandsRef, selectedMessageId, onClose]);
|
|
112
|
+
|
|
113
|
+
useTriageKeyboard({
|
|
114
|
+
// A modal owns the keyboard outright. Suspending the layer is what keeps a
|
|
115
|
+
// second Delete press from reaching a delete while the confirmation for the
|
|
116
|
+
// first one is still on screen.
|
|
117
|
+
enabled: enabled && !blocksKeyboard,
|
|
118
|
+
handlers: {
|
|
119
|
+
// Registered only while a list is mounted to serve them. An unregistered
|
|
120
|
+
// action is never preventDefault-ed, so with no list the browser keeps
|
|
121
|
+
// Enter, Space and ⌘A — select-all-text in the reading pane still works.
|
|
122
|
+
...(hasList
|
|
123
|
+
? {
|
|
124
|
+
focusNext: () => listCommandsRef.current?.focusNext(),
|
|
125
|
+
focusPrevious: () => listCommandsRef.current?.focusPrevious(),
|
|
126
|
+
focusFirst: () => listCommandsRef.current?.focusFirst(),
|
|
127
|
+
focusLast: () => listCommandsRef.current?.focusLast(),
|
|
128
|
+
openFocused: () => listCommandsRef.current?.openFocused(),
|
|
129
|
+
toggleSelect: () => listCommandsRef.current?.toggleSelect(),
|
|
130
|
+
extendSelectDown: () => listCommandsRef.current?.extendSelectDown(),
|
|
131
|
+
extendSelectUp: () => listCommandsRef.current?.extendSelectUp(),
|
|
132
|
+
selectAll: () => listCommandsRef.current?.selectAll(),
|
|
133
|
+
toggleDensity: () => listCommandsRef.current?.toggleDensity(),
|
|
134
|
+
}
|
|
135
|
+
: {}),
|
|
136
|
+
back: goBack,
|
|
137
|
+
...handlers,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
goBack,
|
|
143
|
+
nextMessageId:
|
|
144
|
+
adjacentMessageId(orderedIds, selectedMessageId, "next") ?? undefined,
|
|
145
|
+
previousMessageId:
|
|
146
|
+
adjacentMessageId(orderedIds, selectedMessageId, "previous") ?? undefined,
|
|
147
|
+
};
|
|
148
|
+
};
|