zele 0.3.12 → 0.3.14
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/README.md +8 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +10 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.js +2 -3
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.js +1 -2
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.js +4 -6
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.js +2 -3
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/label.js +4 -5
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail.js +24 -21
- package/dist/commands/mail.js.map +1 -1
- package/dist/gmail-client.d.ts +11 -12
- package/dist/gmail-client.js +55 -40
- package/dist/gmail-client.js.map +1 -1
- package/dist/mail-tui.js +165 -187
- package/dist/mail-tui.js.map +1 -1
- package/dist/output.d.ts +3 -1
- package/dist/output.js +7 -2
- package/dist/output.js.map +1 -1
- package/package.json +7 -5
- package/src/auth.ts +1 -1
- package/src/cli.ts +31 -30
- package/src/commands/attachment.ts +2 -4
- package/src/commands/auth-cmd.ts +1 -2
- package/src/commands/calendar.ts +4 -7
- package/src/commands/draft.ts +2 -3
- package/src/commands/label.ts +4 -4
- package/src/commands/mail.ts +24 -19
- package/src/gmail-client.test.ts +8 -3
- package/src/gmail-client.ts +64 -58
- package/src/mail-tui.tsx +419 -418
- package/src/output.ts +8 -3
- package/bin/zele +0 -27
package/dist/mail-tui.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// Termcast email extension — browse and read emails in a Raycast-like TUI.
|
|
3
3
|
// Uses List with detail view, sections for date grouping, infinite scroll via
|
|
4
4
|
// useCachedPromise pagination, and a dropdown for account selection.
|
|
@@ -19,7 +19,7 @@ if (typeof globalThis.fetch === 'function' &&
|
|
|
19
19
|
}
|
|
20
20
|
import { Action, ActionPanel, Color, Detail, Form, Icon, List, showToast, Toast, useNavigation, showFailureToast, } from 'termcast';
|
|
21
21
|
import { useTerminalDimensions } from '@opentui/react';
|
|
22
|
-
import { useCachedPromise } from '@termcast/utils';
|
|
22
|
+
import { useCachedPromise, useCachedState } from '@termcast/utils';
|
|
23
23
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
24
24
|
import { getClients, getClient, listAccounts, login, logout, } from './auth.js';
|
|
25
25
|
import { isTruthy } from './api-utils.js';
|
|
@@ -30,6 +30,9 @@ import { renderEmailBody, replyParser, formatDate, formatSender, } from './outpu
|
|
|
30
30
|
const DEFAULT_PAGE_SIZE = 25;
|
|
31
31
|
const MIN_PAGE_SIZE = 10;
|
|
32
32
|
const VISIBLE_ROWS_OFFSET = 6;
|
|
33
|
+
/** Spacing mode for the mail list. 'relaxed' renders each item as 3 lines. */
|
|
34
|
+
const LIST_SPACING_MODE = 'relaxed';
|
|
35
|
+
const LINES_PER_ITEM = LIST_SPACING_MODE === 'relaxed' ? 3 : 1;
|
|
33
36
|
const ACCOUNT_COLORS = [
|
|
34
37
|
Color.Blue,
|
|
35
38
|
Color.Green,
|
|
@@ -97,7 +100,9 @@ function threadStatusIcon(thread) {
|
|
|
97
100
|
function getPageSizeFromTerminalHeight(rows) {
|
|
98
101
|
if (typeof rows !== 'number' || rows <= 0)
|
|
99
102
|
return DEFAULT_PAGE_SIZE;
|
|
100
|
-
|
|
103
|
+
const visibleRows = rows - VISIBLE_ROWS_OFFSET;
|
|
104
|
+
const itemCount = Math.floor(visibleRows / LINES_PER_ITEM);
|
|
105
|
+
return Math.max(MIN_PAGE_SIZE, itemCount);
|
|
101
106
|
}
|
|
102
107
|
// ---------------------------------------------------------------------------
|
|
103
108
|
// Data fetching
|
|
@@ -126,7 +131,7 @@ function AccountDropdown({ accounts, value, onChange, onAdded, onRemoved, }) {
|
|
|
126
131
|
}, children: [_jsx(List.Dropdown.Item, { title: 'All Accounts', value: 'all', icon: Icon.Globe }), _jsx(List.Dropdown.Section, { title: 'Accounts', children: accounts.map((a) => (_jsx(List.Dropdown.Item, { title: a.email, value: a.email, icon: {
|
|
127
132
|
source: Icon.Person,
|
|
128
133
|
tintColor: accountColor(a.email),
|
|
129
|
-
} }, a.email))) }), _jsxs(List.Dropdown.Section, { children: [_jsx(List.Dropdown.Item, { title: 'Add Account', value: ADD_ACCOUNT, icon: Icon.Plus }), _jsx(List.Dropdown.Item, { title: 'Manage Accounts', value: MANAGE_ACCOUNTS, icon: Icon.Gear })] })] }));
|
|
134
|
+
} }, a.email))) }), _jsxs(List.Dropdown.Section, { title: 'Manage Accounts', children: [_jsx(List.Dropdown.Item, { title: 'Add Account', value: ADD_ACCOUNT, icon: Icon.Plus }), _jsx(List.Dropdown.Item, { title: 'Manage Accounts', value: MANAGE_ACCOUNTS, icon: Icon.Gear })] })] }));
|
|
130
135
|
}
|
|
131
136
|
// ---------------------------------------------------------------------------
|
|
132
137
|
// Add Account (interactive login)
|
|
@@ -201,83 +206,77 @@ function ManageAccounts({ onAdded, onRemoved, }) {
|
|
|
201
206
|
color: Color.SecondaryText,
|
|
202
207
|
},
|
|
203
208
|
},
|
|
204
|
-
], actions: _jsxs(ActionPanel, { children: [_jsx(Action.CopyToClipboard, { title: 'Copy Email', content: a.email }), _jsx(Action, { title: 'Logout Account', icon: Icon.Trash,
|
|
209
|
+
], actions: _jsxs(ActionPanel, { children: [_jsx(Action.CopyToClipboard, { title: 'Copy Email', content: a.email }), _jsx(Action, { title: 'Logout Account', icon: Icon.Trash, onAction: () => handleRemoved(a.email) })] }) }, `${a.email}-${a.appId}`))), _jsx(List.Item, { title: 'Add Account', icon: Icon.Plus, actions: _jsx(ActionPanel, { children: _jsx(Action.Push, { title: 'Add Account', target: _jsx(AddAccount, { onAdded: handleAdded }) }) }) }, 'add-account')] }));
|
|
205
210
|
}
|
|
206
|
-
|
|
207
|
-
// Reply Form
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
function ReplyForm({ threadId, account, replyAll, revalidate, }) {
|
|
211
|
+
function ComposeForm({ mode, initialAccount, accounts, onSent }) {
|
|
210
212
|
const { pop } = useNavigation();
|
|
211
213
|
const [isLoading, setIsLoading] = useState(false);
|
|
214
|
+
const [selectedAccount, setSelectedAccount] = useState(initialAccount);
|
|
215
|
+
const navigationTitle = mode.type === 'forward'
|
|
216
|
+
? 'Forward'
|
|
217
|
+
: mode.replyAll
|
|
218
|
+
? 'Reply All'
|
|
219
|
+
: 'Reply';
|
|
220
|
+
const bodyPlaceholder = mode.type === 'forward'
|
|
221
|
+
? 'Add a message (optional)...'
|
|
222
|
+
: 'Type your reply...';
|
|
212
223
|
const handleSubmit = async (values) => {
|
|
213
|
-
|
|
224
|
+
// Validate based on mode
|
|
225
|
+
if (mode.type === 'forward' && !values.to?.trim()) {
|
|
214
226
|
await showToast({
|
|
215
227
|
style: Toast.Style.Failure,
|
|
216
|
-
title: '
|
|
228
|
+
title: 'Recipient is required',
|
|
217
229
|
});
|
|
218
230
|
return;
|
|
219
231
|
}
|
|
220
|
-
|
|
221
|
-
const { client } = await getClient([account]);
|
|
222
|
-
const result = await client.replyToThread({
|
|
223
|
-
threadId,
|
|
224
|
-
body: values.body,
|
|
225
|
-
replyAll,
|
|
226
|
-
});
|
|
227
|
-
setIsLoading(false);
|
|
228
|
-
if (result instanceof Error) {
|
|
229
|
-
await showFailureToast(result, { title: 'Failed to send reply' });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
await showToast({ style: Toast.Style.Success, title: 'Reply sent' });
|
|
233
|
-
revalidate();
|
|
234
|
-
pop();
|
|
235
|
-
};
|
|
236
|
-
return (_jsx(Form, { isLoading: isLoading, navigationTitle: replyAll ? 'Reply All' : 'Reply', actions: _jsx(ActionPanel, { children: _jsx(Action.SubmitForm, { title: 'Send Reply', onSubmit: handleSubmit }) }), children: _jsx(Form.TextArea, { id: 'body', title: 'Message', placeholder: 'Type your reply...' }) }));
|
|
237
|
-
}
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
// Forward Form
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
function ForwardForm({ threadId, account, revalidate, }) {
|
|
242
|
-
const { pop } = useNavigation();
|
|
243
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
244
|
-
const handleSubmit = async (values) => {
|
|
245
|
-
if (!values.to?.trim()) {
|
|
232
|
+
if (mode.type !== 'forward' && !values.body?.trim()) {
|
|
246
233
|
await showToast({
|
|
247
234
|
style: Toast.Style.Failure,
|
|
248
|
-
title: '
|
|
235
|
+
title: 'Message is required',
|
|
249
236
|
});
|
|
250
237
|
return;
|
|
251
238
|
}
|
|
252
239
|
setIsLoading(true);
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
240
|
+
const { client } = await getClient([selectedAccount]);
|
|
241
|
+
let result;
|
|
242
|
+
if (mode.type === 'forward') {
|
|
243
|
+
const recipients = (values.to ?? '')
|
|
244
|
+
.split(',')
|
|
245
|
+
.map((e) => ({ email: e.trim() }))
|
|
246
|
+
.filter((e) => e.email);
|
|
247
|
+
result = await client.forwardThread({
|
|
248
|
+
threadId: mode.threadId,
|
|
249
|
+
to: recipients,
|
|
250
|
+
body: values.body || undefined,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
result = await client.replyToThread({
|
|
255
|
+
threadId: mode.threadId,
|
|
256
|
+
body: values.body ?? '',
|
|
257
|
+
replyAll: mode.replyAll,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
263
260
|
setIsLoading(false);
|
|
264
261
|
if (result instanceof Error) {
|
|
265
|
-
await showFailureToast(result, {
|
|
262
|
+
await showFailureToast(result, {
|
|
263
|
+
title: mode.type === 'forward' ? 'Failed to forward' : 'Failed to send reply',
|
|
264
|
+
});
|
|
266
265
|
return;
|
|
267
266
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
267
|
+
const successTitle = mode.type === 'forward'
|
|
268
|
+
? `Forwarded to ${values.to}`
|
|
269
|
+
: 'Reply sent';
|
|
270
|
+
await showToast({ style: Toast.Style.Success, title: successTitle });
|
|
271
|
+
onSent?.();
|
|
273
272
|
pop();
|
|
274
273
|
};
|
|
275
|
-
return (_jsxs(Form, { isLoading: isLoading, navigationTitle:
|
|
274
|
+
return (_jsxs(Form, { isLoading: isLoading, navigationTitle: navigationTitle, actions: _jsx(ActionPanel, { children: _jsx(Action.SubmitForm, { title: mode.type === 'forward' ? 'Forward' : 'Send Reply', onSubmit: handleSubmit }) }), children: [accounts.length > 1 && (_jsx(Form.Dropdown, { id: 'account', title: 'From', value: selectedAccount, onChange: (v) => setSelectedAccount(Array.isArray(v) ? v[0] ?? initialAccount : v), children: accounts.map((a) => (_jsx(Form.Dropdown.Item, { value: a.email, title: a.email }, a.email))) })), mode.type === 'forward' && (_jsx(Form.TextField, { id: 'to', title: 'To', placeholder: 'recipient@example.com' })), _jsx(Form.TextArea, { id: 'body', title: 'Message', placeholder: bodyPlaceholder })] }));
|
|
276
275
|
}
|
|
277
276
|
// ---------------------------------------------------------------------------
|
|
278
277
|
// Thread Detail (full thread view, pushed via Enter)
|
|
279
278
|
// ---------------------------------------------------------------------------
|
|
280
|
-
function ThreadDetail({ threadId, account, revalidate, }) {
|
|
279
|
+
function ThreadDetail({ threadId, account, accounts, revalidate, }) {
|
|
281
280
|
const thread = useCachedPromise(async (tid, acct) => {
|
|
282
281
|
const { client } = await getClient([acct]);
|
|
283
282
|
const result = await client.getThread({ threadId: tid });
|
|
@@ -322,20 +321,34 @@ function ThreadDetail({ threadId, account, revalidate, }) {
|
|
|
322
321
|
.filter((l) => typeof l === 'string' && !l.startsWith('Label_')) // skip internal IDs
|
|
323
322
|
.slice(0, 10);
|
|
324
323
|
const latestMsg = messages[messages.length - 1];
|
|
325
|
-
return (_jsx(Detail, { navigationTitle: t.subject, markdown: markdown, metadata: _jsxs(Detail.Metadata, { children: [_jsx(Detail.Metadata.Label, { title: 'From', text: formatSender(latestMsg.from) }), _jsx(Detail.Metadata.Label, { title: 'To', text: latestMsg.to.map((r) => r.name || r.email).join(', ') }), latestMsg.cc && latestMsg.cc.length > 0 && (_jsx(Detail.Metadata.Label, { title: 'Cc', text: latestMsg.cc.map((r) => r.name || r.email).join(', ') })), _jsx(Detail.Metadata.Label, { title: 'Date', text: latestMsg.date }), _jsx(Detail.Metadata.Separator, {}), _jsx(Detail.Metadata.Label, { title: 'Messages', text: String(t.messageCount) }), _jsx(Detail.Metadata.Label, { title: 'Participants', text: [...participants.values()].join(', ') }), labels.length > 0 && (_jsx(Detail.Metadata.TagList, { title: 'Labels', children: labels.map((l) => (_jsx(Detail.Metadata.TagList.Item, { text: l, color: labelColor(l) }, l))) })), _jsx(Detail.Metadata.Separator, {}), _jsx(Detail.Metadata.Label, { title: 'Thread ID', text: t.id }), _jsx(Detail.Metadata.Label, { title: 'Account', text: account })] }), actions: _jsxs(ActionPanel, { children: [_jsxs(ActionPanel.Section, { title: 'Reply & Forward', children: [_jsx(Action.Push, { title: 'Reply', icon: Icon.Reply, shortcut: { modifiers: ['ctrl'], key: 'r' }, target: _jsx(
|
|
324
|
+
return (_jsx(Detail, { navigationTitle: t.subject, markdown: markdown, metadata: _jsxs(Detail.Metadata, { children: [_jsx(Detail.Metadata.Label, { title: 'From', text: formatSender(latestMsg.from) }), _jsx(Detail.Metadata.Label, { title: 'To', text: latestMsg.to.map((r) => r.name || r.email).join(', ') }), latestMsg.cc && latestMsg.cc.length > 0 && (_jsx(Detail.Metadata.Label, { title: 'Cc', text: latestMsg.cc.map((r) => r.name || r.email).join(', ') })), _jsx(Detail.Metadata.Label, { title: 'Date', text: latestMsg.date }), _jsx(Detail.Metadata.Separator, {}), _jsx(Detail.Metadata.Label, { title: 'Messages', text: String(t.messageCount) }), _jsx(Detail.Metadata.Label, { title: 'Participants', text: [...participants.values()].join(', ') }), labels.length > 0 && (_jsx(Detail.Metadata.TagList, { title: 'Labels', children: labels.map((l) => (_jsx(Detail.Metadata.TagList.Item, { text: l, color: labelColor(l) }, l))) })), _jsx(Detail.Metadata.Separator, {}), _jsx(Detail.Metadata.Label, { title: 'Thread ID', text: t.id }), _jsx(Detail.Metadata.Label, { title: 'Account', text: account })] }), actions: _jsxs(ActionPanel, { children: [_jsxs(ActionPanel.Section, { title: 'Reply & Forward', children: [_jsx(Action.Push, { title: 'Reply', icon: Icon.Reply, shortcut: { modifiers: ['ctrl'], key: 'r' }, target: _jsx(ComposeForm, { mode: { type: 'reply', threadId }, initialAccount: account, accounts: accounts, onSent: revalidate }) }), _jsx(Action.Push, { title: 'Reply All', icon: Icon.Reply, shortcut: {
|
|
326
325
|
modifiers: ['ctrl', 'shift'],
|
|
327
326
|
key: 'r',
|
|
328
|
-
}, target: _jsx(
|
|
327
|
+
}, target: _jsx(ComposeForm, { mode: { type: 'reply', threadId, replyAll: true }, initialAccount: account, accounts: accounts, onSent: revalidate }) }), _jsx(Action.Push, { title: 'Forward', icon: Icon.Forward, shortcut: { modifiers: ['ctrl'], key: 'f' }, target: _jsx(ComposeForm, { mode: { type: 'forward', threadId }, initialAccount: account, accounts: accounts, onSent: revalidate }) })] }), _jsxs(ActionPanel.Section, { title: 'Copy', children: [_jsx(Action.CopyToClipboard, { title: 'Copy Thread ID', content: t.id }), _jsx(Action.CopyToClipboard, { title: 'Copy Subject', content: t.subject }), latestMsg && (_jsx(Action.CopyToClipboard, { title: 'Copy Email Body', content: renderEmailBody(latestMsg.body, latestMsg.mimeType) }))] })] }) }));
|
|
329
328
|
}
|
|
330
329
|
// ---------------------------------------------------------------------------
|
|
331
330
|
// Main Command
|
|
332
331
|
// ---------------------------------------------------------------------------
|
|
332
|
+
const CACHE_NAMESPACE = 'mail-tui';
|
|
333
333
|
export default function Command() {
|
|
334
|
-
const
|
|
334
|
+
const { push } = useNavigation();
|
|
335
|
+
const [selectedAccount, setSelectedAccount] = useCachedState('selectedAccount', 'all', { cacheNamespace: CACHE_NAMESPACE });
|
|
335
336
|
const [searchText, setSearchText] = useState('');
|
|
336
|
-
const [isShowingDetail, setIsShowingDetail] =
|
|
337
|
+
const [isShowingDetail, setIsShowingDetail] = useCachedState('isShowingDetail', true, { cacheNamespace: CACHE_NAMESPACE });
|
|
337
338
|
const [selectedThreads, setSelectedThreads] = useState([]);
|
|
339
|
+
const [activeMutations, setActiveMutations] = useState(0);
|
|
340
|
+
const isMutating = activeMutations > 0;
|
|
338
341
|
const { height: terminalRows } = useTerminalDimensions();
|
|
342
|
+
/** Wrap async mutation calls to track global loading state. */
|
|
343
|
+
const withMutation = useCallback(async (fn) => {
|
|
344
|
+
setActiveMutations((n) => n + 1);
|
|
345
|
+
try {
|
|
346
|
+
return await fn();
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
setActiveMutations((n) => n - 1);
|
|
350
|
+
}
|
|
351
|
+
}, []);
|
|
339
352
|
const pageSize = getPageSizeFromTerminalHeight(terminalRows);
|
|
340
353
|
const accounts = useAccounts();
|
|
341
354
|
const accountList = accounts.data ?? [];
|
|
@@ -462,34 +475,36 @@ export default function Command() {
|
|
|
462
475
|
const handleBulkAction = useCallback(async (actionName, fn) => {
|
|
463
476
|
if (selectedThreads.length === 0)
|
|
464
477
|
return;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
for (const [acct, ids] of byAccount) {
|
|
476
|
-
const { client } = await getClient([acct]);
|
|
477
|
-
const result = await fn(client, ids);
|
|
478
|
-
if (result instanceof Error) {
|
|
479
|
-
await showFailureToast(result, {
|
|
480
|
-
title: `Failed to ${actionName}`,
|
|
481
|
-
});
|
|
482
|
-
return;
|
|
478
|
+
await withMutation(async () => {
|
|
479
|
+
// Group selected threads by account
|
|
480
|
+
const byAccount = new Map();
|
|
481
|
+
for (const tid of selectedThreads) {
|
|
482
|
+
const thread = allThreads.find((t) => t.id === tid);
|
|
483
|
+
if (!thread)
|
|
484
|
+
continue;
|
|
485
|
+
const list = byAccount.get(thread.account) ?? [];
|
|
486
|
+
list.push(tid);
|
|
487
|
+
byAccount.set(thread.account, list);
|
|
483
488
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
489
|
+
for (const [acct, ids] of byAccount) {
|
|
490
|
+
const { client } = await getClient([acct]);
|
|
491
|
+
const result = await fn(client, ids);
|
|
492
|
+
if (result instanceof Error) {
|
|
493
|
+
await showFailureToast(result, {
|
|
494
|
+
title: `Failed to ${actionName}`,
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
await showToast({
|
|
500
|
+
style: Toast.Style.Success,
|
|
501
|
+
title: `${actionName}: ${selectedThreads.length} thread(s)`,
|
|
502
|
+
});
|
|
503
|
+
setSelectedThreads([]);
|
|
504
|
+
revalidate();
|
|
488
505
|
});
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}, [selectedThreads, allThreads, revalidate]);
|
|
492
|
-
return (_jsx(List, { isLoading: isLoading || accounts.isLoading, isShowingDetail: isShowingDetail, searchBarPlaceholder: 'Search emails...', onSearchTextChange: setSearchText, throttle: true, pagination: pagination ? { ...pagination, pageSize } : undefined, searchBarAccessory: accountList.length > 0 ? (_jsx(AccountDropdown, { accounts: accountList, value: selectedAccount, onChange: setSelectedAccount, onAdded: handleAccountAdded, onRemoved: handleAccountRemoved })) : undefined, children: sections.map((section) => (_jsx(List.Section, { title: section.name, children: section.threads.map((thread) => {
|
|
506
|
+
}, [selectedThreads, allThreads, revalidate, withMutation]);
|
|
507
|
+
return (_jsx(List, { isLoading: isLoading || accounts.isLoading || isMutating, isShowingDetail: isShowingDetail, spacingMode: LIST_SPACING_MODE, searchBarPlaceholder: 'Search emails...', onSearchTextChange: setSearchText, throttle: true, pagination: pagination ? { ...pagination, pageSize } : undefined, searchBarAccessory: accountList.length > 0 ? (_jsx(AccountDropdown, { accounts: accountList, value: selectedAccount, onChange: setSelectedAccount, onAdded: handleAccountAdded, onRemoved: handleAccountRemoved })) : undefined, children: sections.map((section) => (_jsx(List.Section, { title: section.name, children: section.threads.map((thread) => {
|
|
493
508
|
const isSelected = selectedThreads.includes(thread.id);
|
|
494
509
|
const hasSelection = selectedThreads.length > 0;
|
|
495
510
|
// Icon: selection mode or status
|
|
@@ -527,114 +542,77 @@ export default function Command() {
|
|
|
527
542
|
thread.from.email,
|
|
528
543
|
thread.from.name ?? '',
|
|
529
544
|
thread.account,
|
|
530
|
-
], detail: detail, actions:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
545
|
+
], detail: detail, actions: _jsx(ActionPanel, { children: hasSelection ? (
|
|
546
|
+
// ─────────────────────────────────────────────
|
|
547
|
+
// SELECTION MODE
|
|
548
|
+
// ─────────────────────────────────────────────
|
|
549
|
+
_jsxs(_Fragment, { children: [_jsxs(ActionPanel.Section, { title: 'Selection', children: [_jsx(Action, { title: isSelected ? 'Deselect Thread' : 'Select Thread', icon: isSelected ? Icon.Circle : Icon.CheckCircle, shortcut: { modifiers: ['ctrl'], key: 'x' }, onAction: () => toggleSelection(thread.id) }), _jsx(Action, { title: `Archive ${selectedThreads.length} Selected`, icon: Icon.Tray, onAction: () => handleBulkAction('Archived', (c, ids) => c.archive({ threadIds: ids })) }), _jsx(Action, { title: `Mark ${selectedThreads.length} as Read`, icon: Icon.Eye, onAction: () => handleBulkAction('Marked as read', (c, ids) => c.markAsRead({ threadIds: ids })) }), _jsx(Action, { title: `Star ${selectedThreads.length} Selected`, icon: Icon.Star, onAction: () => handleBulkAction('Starred', (c, ids) => c.star({ threadIds: ids })) }), _jsx(Action, { title: `Trash ${selectedThreads.length} Selected`, icon: Icon.Trash, onAction: () => handleBulkAction('Trashed', async (c, ids) => {
|
|
550
|
+
for (const id of ids) {
|
|
551
|
+
await c.trash({ threadId: id });
|
|
552
|
+
}
|
|
553
|
+
}) }), _jsx(Action, { title: 'Deselect All', icon: Icon.XMarkCircle, onAction: () => setSelectedThreads([]) })] }), _jsxs(ActionPanel.Section, { children: [_jsx(Action, { title: 'Refresh', icon: Icon.ArrowClockwise, shortcut: { modifiers: ['ctrl', 'shift'], key: 'r' }, onAction: () => revalidate() }), _jsx(Action, { title: 'Toggle Detail', icon: Icon.Sidebar, shortcut: { modifiers: ['ctrl'], key: 'd' }, onAction: () => setIsShowingDetail((v) => !v) })] })] })) : (
|
|
554
|
+
// ─────────────────────────────────────────────
|
|
555
|
+
// NORMAL MODE
|
|
556
|
+
// ─────────────────────────────────────────────
|
|
557
|
+
_jsxs(_Fragment, { children: [_jsxs(ActionPanel.Section, { children: [_jsx(Action, { title: 'Open Thread', icon: Icon.Eye, onAction: async () => {
|
|
558
|
+
push(_jsx(ThreadDetail, { threadId: thread.id, account: thread.account, accounts: accountList, revalidate: revalidate }));
|
|
559
|
+
if (thread.unread) {
|
|
560
|
+
const { client } = await getClient([thread.account]);
|
|
561
|
+
const result = await client.markAsRead({ threadIds: [thread.id] });
|
|
562
|
+
if (result instanceof Error)
|
|
563
|
+
return;
|
|
564
|
+
revalidate();
|
|
565
|
+
}
|
|
566
|
+
} }), _jsx(Action, { title: 'Select Thread', icon: Icon.CheckCircle, shortcut: { modifiers: ['ctrl'], key: 'x' }, onAction: () => toggleSelection(thread.id) }), _jsx(Action, { title: thread.unread ? 'Mark as Read' : 'Mark as Unread', icon: thread.unread ? Icon.Eye : Icon.EyeDisabled, shortcut: { modifiers: ['ctrl'], key: 'u' }, onAction: () => withMutation(async () => {
|
|
567
|
+
const { client } = await getClient([thread.account]);
|
|
568
|
+
const result = thread.unread
|
|
569
|
+
? await client.markAsRead({ threadIds: [thread.id] })
|
|
570
|
+
: await client.markAsUnread({ threadIds: [thread.id] });
|
|
571
|
+
if (result instanceof Error) {
|
|
572
|
+
await showFailureToast(result);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
await showToast({
|
|
576
|
+
style: Toast.Style.Success,
|
|
577
|
+
title: thread.unread ? 'Marked as read' : 'Marked as unread',
|
|
540
578
|
});
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
threadIds: [thread.id],
|
|
553
|
-
})
|
|
554
|
-
: await client.markAsUnread({
|
|
555
|
-
threadIds: [thread.id],
|
|
579
|
+
revalidate();
|
|
580
|
+
}) }), _jsx(Action, { title: 'Archive', icon: Icon.Tray, shortcut: { modifiers: ['ctrl'], key: 'e' }, onAction: () => withMutation(async () => {
|
|
581
|
+
const { client } = await getClient([thread.account]);
|
|
582
|
+
const result = await client.archive({ threadIds: [thread.id] });
|
|
583
|
+
if (result instanceof Error) {
|
|
584
|
+
await showFailureToast(result);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
await showToast({
|
|
588
|
+
style: Toast.Style.Success,
|
|
589
|
+
title: 'Archived',
|
|
556
590
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}, onAction: async () => {
|
|
572
|
-
const { client } = await getClient([thread.account]);
|
|
573
|
-
const result = await client.archive({
|
|
574
|
-
threadIds: [thread.id],
|
|
575
|
-
});
|
|
576
|
-
if (result instanceof Error) {
|
|
577
|
-
await showFailureToast(result);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
await showToast({
|
|
581
|
-
style: Toast.Style.Success,
|
|
582
|
-
title: 'Archived',
|
|
583
|
-
});
|
|
584
|
-
revalidate();
|
|
585
|
-
} }), _jsx(Action, { title: thread.labelIds.includes('STARRED')
|
|
586
|
-
? 'Unstar'
|
|
587
|
-
: 'Star', icon: Icon.Star, shortcut: {
|
|
588
|
-
modifiers: ['ctrl'],
|
|
589
|
-
key: 's',
|
|
590
|
-
}, onAction: async () => {
|
|
591
|
-
const { client } = await getClient([thread.account]);
|
|
592
|
-
const isStarred = thread.labelIds.includes('STARRED');
|
|
593
|
-
const result = isStarred
|
|
594
|
-
? await client.unstar({
|
|
595
|
-
threadIds: [thread.id],
|
|
596
|
-
})
|
|
597
|
-
: await client.star({
|
|
598
|
-
threadIds: [thread.id],
|
|
591
|
+
revalidate();
|
|
592
|
+
}) }), _jsx(Action, { title: thread.labelIds.includes('STARRED') ? 'Unstar' : 'Star', icon: Icon.Star, shortcut: { modifiers: ['ctrl'], key: 's' }, onAction: () => withMutation(async () => {
|
|
593
|
+
const { client } = await getClient([thread.account]);
|
|
594
|
+
const isStarred = thread.labelIds.includes('STARRED');
|
|
595
|
+
const result = isStarred
|
|
596
|
+
? await client.unstar({ threadIds: [thread.id] })
|
|
597
|
+
: await client.star({ threadIds: [thread.id] });
|
|
598
|
+
if (result instanceof Error) {
|
|
599
|
+
await showFailureToast(result);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await showToast({
|
|
603
|
+
style: Toast.Style.Success,
|
|
604
|
+
title: isStarred ? 'Unstarred' : 'Starred',
|
|
599
605
|
});
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
606
|
+
revalidate();
|
|
607
|
+
}) })] }), _jsxs(ActionPanel.Section, { title: 'Reply & Forward', children: [_jsx(Action.Push, { title: 'Reply', icon: Icon.Reply, shortcut: { modifiers: ['ctrl'], key: 'r' }, target: _jsx(ComposeForm, { mode: { type: 'reply', threadId: thread.id }, initialAccount: thread.account, accounts: accountList, onSent: revalidate }) }), _jsx(Action.Push, { title: 'Reply All', icon: Icon.Reply, shortcut: { modifiers: ['ctrl', 'shift'], key: 'r' }, target: _jsx(ComposeForm, { mode: { type: 'reply', threadId: thread.id, replyAll: true }, initialAccount: thread.account, accounts: accountList, onSent: revalidate }) }), _jsx(Action.Push, { title: 'Forward', icon: Icon.Forward, shortcut: { modifiers: ['ctrl'], key: 'f' }, target: _jsx(ComposeForm, { mode: { type: 'forward', threadId: thread.id }, initialAccount: thread.account, accounts: accountList, onSent: revalidate }) })] }), _jsxs(ActionPanel.Section, { title: 'Copy', children: [_jsx(Action.CopyToClipboard, { title: 'Copy Thread ID', content: thread.id }), _jsx(Action.CopyToClipboard, { title: 'Copy Subject', content: thread.subject }), _jsx(Action.CopyToClipboard, { title: 'Copy Sender Email', content: thread.from.email })] }), _jsx(ActionPanel.Section, { children: _jsx(Action, { title: 'Trash', icon: Icon.Trash, shortcut: { modifiers: ['ctrl'], key: 'backspace' }, onAction: () => withMutation(async () => {
|
|
608
|
+
const { client } = await getClient([thread.account]);
|
|
609
|
+
await client.trash({ threadId: thread.id });
|
|
604
610
|
await showToast({
|
|
605
611
|
style: Toast.Style.Success,
|
|
606
|
-
title:
|
|
612
|
+
title: 'Trashed',
|
|
607
613
|
});
|
|
608
614
|
revalidate();
|
|
609
|
-
} })
|
|
610
|
-
modifiers: ['ctrl'],
|
|
611
|
-
key: 'r',
|
|
612
|
-
}, target: _jsx(ReplyForm, { threadId: thread.id, account: thread.account, revalidate: revalidate }) }), _jsx(Action.Push, { title: 'Reply All', icon: Icon.Reply, shortcut: {
|
|
613
|
-
modifiers: ['ctrl', 'shift'],
|
|
614
|
-
key: 'r',
|
|
615
|
-
}, target: _jsx(ReplyForm, { threadId: thread.id, account: thread.account, replyAll: true, revalidate: revalidate }) }), _jsx(Action.Push, { title: 'Forward', icon: Icon.Forward, shortcut: {
|
|
616
|
-
modifiers: ['ctrl'],
|
|
617
|
-
key: 'f',
|
|
618
|
-
}, target: _jsx(ForwardForm, { threadId: thread.id, account: thread.account, revalidate: revalidate }) })] }), _jsxs(ActionPanel.Section, { title: 'Copy', children: [_jsx(Action.CopyToClipboard, { title: 'Copy Thread ID', content: thread.id }), _jsx(Action.CopyToClipboard, { title: 'Copy Subject', content: thread.subject }), _jsx(Action.CopyToClipboard, { title: 'Copy Sender Email', content: thread.from.email })] }), _jsx(ActionPanel.Section, { children: _jsx(Action, { title: 'Trash', icon: Icon.Trash, style: Action.Style.Destructive, shortcut: {
|
|
619
|
-
modifiers: ['ctrl'],
|
|
620
|
-
key: 'backspace',
|
|
621
|
-
}, onAction: async () => {
|
|
622
|
-
const { client } = await getClient([thread.account]);
|
|
623
|
-
await client.trash({
|
|
624
|
-
threadId: thread.id,
|
|
625
|
-
});
|
|
626
|
-
await showToast({
|
|
627
|
-
style: Toast.Style.Success,
|
|
628
|
-
title: 'Trashed',
|
|
629
|
-
});
|
|
630
|
-
revalidate();
|
|
631
|
-
} }) }), _jsxs(ActionPanel.Section, { children: [_jsx(Action, { title: 'Refresh', icon: Icon.ArrowClockwise, shortcut: {
|
|
632
|
-
modifiers: ['ctrl', 'shift'],
|
|
633
|
-
key: 'r',
|
|
634
|
-
}, onAction: () => revalidate() }), _jsx(Action, { title: 'Toggle Detail', icon: Icon.Sidebar, shortcut: {
|
|
635
|
-
modifiers: ['ctrl'],
|
|
636
|
-
key: 'd',
|
|
637
|
-
}, onAction: () => setIsShowingDetail((v) => !v) })] })] }) }, `${thread.account}-${thread.id}`));
|
|
615
|
+
}) }) }), _jsxs(ActionPanel.Section, { children: [_jsx(Action, { title: 'Refresh', icon: Icon.ArrowClockwise, shortcut: { modifiers: ['ctrl', 'shift'], key: 'r' }, onAction: () => revalidate() }), _jsx(Action, { title: 'Toggle Detail', icon: Icon.Sidebar, shortcut: { modifiers: ['ctrl'], key: 'd' }, onAction: () => setIsShowingDetail((v) => !v) })] })] })) }) }, `${thread.account}-${thread.id}`));
|
|
638
616
|
}) }, section.name))) }));
|
|
639
617
|
}
|
|
640
618
|
// ---------------------------------------------------------------------------
|