zele 0.3.10 → 0.3.12
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/dist/cli.js +1 -1
- package/dist/generated/internal/class.js +2 -10
- package/dist/generated/internal/class.js.map +1 -1
- package/dist/generated/internal/prismaNamespace.d.ts +2 -2
- package/dist/generated/internal/prismaNamespace.js +4 -4
- package/dist/mail-tui.js +200 -52
- package/dist/mail-tui.js.map +1 -1
- package/package.json +6 -5
- package/src/cli.ts +1 -1
- package/src/generated/internal/class.ts +2 -10
- package/src/generated/internal/prismaNamespace.ts +4 -4
- package/src/mail-tui.tsx +408 -130
package/src/mail-tui.tsx
CHANGED
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
// the Raycast UI runtime) but without fetch. gaxios sees window exists → tries
|
|
9
9
|
// window.fetch → gets undefined → "fetchImpl is not a function". Fix: ensure
|
|
10
10
|
// window.fetch is set to the native Bun fetch.
|
|
11
|
-
const globalWithWindow = globalThis as unknown as {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const globalWithWindow = globalThis as unknown as {
|
|
12
|
+
window?: { fetch?: typeof globalThis.fetch }
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
typeof globalThis.fetch === 'function' &&
|
|
16
|
+
typeof globalWithWindow.window?.fetch !== 'function'
|
|
17
|
+
) {
|
|
18
|
+
globalWithWindow.window = {
|
|
19
|
+
...globalWithWindow.window,
|
|
20
|
+
fetch: globalThis.fetch,
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
import {
|
|
@@ -30,10 +38,22 @@ import { useTerminalDimensions } from '@opentui/react'
|
|
|
30
38
|
import { useCachedPromise } from '@termcast/utils'
|
|
31
39
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
32
40
|
|
|
33
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
getClients,
|
|
43
|
+
getClient,
|
|
44
|
+
listAccounts,
|
|
45
|
+
login,
|
|
46
|
+
logout,
|
|
47
|
+
type AuthStatus,
|
|
48
|
+
} from './auth.js'
|
|
34
49
|
import type { GmailClient, ThreadListItem, ThreadData } from './gmail-client.js'
|
|
35
50
|
import { AuthError, ApiError, isTruthy } from './api-utils.js'
|
|
36
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
renderEmailBody,
|
|
53
|
+
replyParser,
|
|
54
|
+
formatDate,
|
|
55
|
+
formatSender,
|
|
56
|
+
} from './output.js'
|
|
37
57
|
|
|
38
58
|
// ---------------------------------------------------------------------------
|
|
39
59
|
// Constants
|
|
@@ -43,7 +63,13 @@ const DEFAULT_PAGE_SIZE = 25
|
|
|
43
63
|
const MIN_PAGE_SIZE = 10
|
|
44
64
|
const VISIBLE_ROWS_OFFSET = 6
|
|
45
65
|
|
|
46
|
-
const ACCOUNT_COLORS = [
|
|
66
|
+
const ACCOUNT_COLORS = [
|
|
67
|
+
Color.Blue,
|
|
68
|
+
Color.Green,
|
|
69
|
+
Color.Purple,
|
|
70
|
+
Color.Orange,
|
|
71
|
+
Color.Magenta,
|
|
72
|
+
]
|
|
47
73
|
|
|
48
74
|
const ADD_ACCOUNT = '__add_account__'
|
|
49
75
|
const MANAGE_ACCOUNTS = '__manage_accounts__'
|
|
@@ -80,15 +106,26 @@ function dateSection(dateStr: string): string {
|
|
|
80
106
|
return 'Older'
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
const SECTION_ORDER = [
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
const SECTION_ORDER = [
|
|
110
|
+
'Last 10 Minutes',
|
|
111
|
+
'Last Hour',
|
|
112
|
+
'Today',
|
|
113
|
+
'Yesterday',
|
|
114
|
+
'This Week',
|
|
115
|
+
'This Month',
|
|
116
|
+
'Older',
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
function threadStatusIcon(thread: ThreadListItem & { starred?: boolean }): {
|
|
120
|
+
source: (typeof Icon)[keyof typeof Icon]
|
|
121
|
+
tintColor: string
|
|
122
|
+
} {
|
|
86
123
|
const unread = thread.unread
|
|
87
124
|
const starred = thread.labelIds?.includes('STARRED') ?? false
|
|
88
125
|
|
|
89
126
|
if (unread && starred) return { source: Icon.Star, tintColor: Color.Red }
|
|
90
|
-
if (unread) return { source: Icon.CircleFilled, tintColor: Color.
|
|
91
|
-
if (starred) return { source: Icon.Star, tintColor: Color.
|
|
127
|
+
if (unread) return { source: Icon.CircleFilled, tintColor: Color.Yellow }
|
|
128
|
+
if (starred) return { source: Icon.Star, tintColor: Color.Orange }
|
|
92
129
|
return { source: Icon.Circle, tintColor: Color.SecondaryText }
|
|
93
130
|
}
|
|
94
131
|
|
|
@@ -138,7 +175,7 @@ function AccountDropdown({
|
|
|
138
175
|
|
|
139
176
|
return (
|
|
140
177
|
<List.Dropdown
|
|
141
|
-
tooltip=
|
|
178
|
+
tooltip='Account'
|
|
142
179
|
value={value}
|
|
143
180
|
onChange={(newValue) => {
|
|
144
181
|
if (newValue === ADD_ACCOUNT) {
|
|
@@ -152,20 +189,31 @@ function AccountDropdown({
|
|
|
152
189
|
onChange(newValue)
|
|
153
190
|
}}
|
|
154
191
|
>
|
|
155
|
-
<List.Dropdown.Item title=
|
|
156
|
-
<List.Dropdown.Section title=
|
|
192
|
+
<List.Dropdown.Item title='All Accounts' value='all' icon={Icon.Globe} />
|
|
193
|
+
<List.Dropdown.Section title='Accounts'>
|
|
157
194
|
{accounts.map((a) => (
|
|
158
195
|
<List.Dropdown.Item
|
|
159
196
|
key={a.email}
|
|
160
197
|
title={a.email}
|
|
161
198
|
value={a.email}
|
|
162
|
-
icon={{
|
|
199
|
+
icon={{
|
|
200
|
+
source: Icon.Person,
|
|
201
|
+
tintColor: accountColor(a.email),
|
|
202
|
+
}}
|
|
163
203
|
/>
|
|
164
204
|
))}
|
|
165
205
|
</List.Dropdown.Section>
|
|
166
206
|
<List.Dropdown.Section>
|
|
167
|
-
<List.Dropdown.Item
|
|
168
|
-
|
|
207
|
+
<List.Dropdown.Item
|
|
208
|
+
title='Add Account'
|
|
209
|
+
value={ADD_ACCOUNT}
|
|
210
|
+
icon={Icon.Plus}
|
|
211
|
+
/>
|
|
212
|
+
<List.Dropdown.Item
|
|
213
|
+
title='Manage Accounts'
|
|
214
|
+
value={MANAGE_ACCOUNTS}
|
|
215
|
+
icon={Icon.Gear}
|
|
216
|
+
/>
|
|
169
217
|
</List.Dropdown.Section>
|
|
170
218
|
</List.Dropdown>
|
|
171
219
|
)
|
|
@@ -201,7 +249,10 @@ function AddAccount({
|
|
|
201
249
|
}
|
|
202
250
|
|
|
203
251
|
await onAdded?.(result.email)
|
|
204
|
-
await showToast({
|
|
252
|
+
await showToast({
|
|
253
|
+
style: Toast.Style.Success,
|
|
254
|
+
title: `Added ${result.email}`,
|
|
255
|
+
})
|
|
205
256
|
pop()
|
|
206
257
|
}
|
|
207
258
|
|
|
@@ -213,7 +264,7 @@ function AddAccount({
|
|
|
213
264
|
|
|
214
265
|
return (
|
|
215
266
|
<Detail
|
|
216
|
-
navigationTitle=
|
|
267
|
+
navigationTitle='Add Account'
|
|
217
268
|
markdown={
|
|
218
269
|
`# Add Account\n\n` +
|
|
219
270
|
`The browser opens automatically for Google sign-in.\n\n` +
|
|
@@ -254,28 +305,43 @@ function ManageAccounts({
|
|
|
254
305
|
const handleRemoved = async (email: string) => {
|
|
255
306
|
const result = await logout(email)
|
|
256
307
|
if (result instanceof Error) {
|
|
257
|
-
await showFailureToast(result, {
|
|
308
|
+
await showFailureToast(result, {
|
|
309
|
+
title: `Failed to remove ${email}`,
|
|
310
|
+
})
|
|
258
311
|
return
|
|
259
312
|
}
|
|
260
313
|
|
|
261
314
|
await accounts.revalidate()
|
|
262
315
|
await onRemoved?.(email)
|
|
263
|
-
await showToast({
|
|
316
|
+
await showToast({
|
|
317
|
+
style: Toast.Style.Success,
|
|
318
|
+
title: `Removed ${email}`,
|
|
319
|
+
})
|
|
264
320
|
}
|
|
265
321
|
|
|
266
322
|
return (
|
|
267
|
-
<List navigationTitle=
|
|
323
|
+
<List navigationTitle='Manage Accounts' isLoading={accounts.isLoading}>
|
|
268
324
|
{accounts.data?.map((a: AuthStatus) => (
|
|
269
325
|
<List.Item
|
|
270
326
|
key={`${a.email}-${a.appId}`}
|
|
271
327
|
title={a.email}
|
|
272
|
-
icon={{
|
|
273
|
-
|
|
328
|
+
icon={{
|
|
329
|
+
source: Icon.Person,
|
|
330
|
+
tintColor: accountColor(a.email),
|
|
331
|
+
}}
|
|
332
|
+
accessories={[
|
|
333
|
+
{
|
|
334
|
+
tag: {
|
|
335
|
+
value: a.appId.slice(0, 12) + '...',
|
|
336
|
+
color: Color.SecondaryText,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
]}
|
|
274
340
|
actions={
|
|
275
341
|
<ActionPanel>
|
|
276
|
-
<Action.CopyToClipboard title=
|
|
342
|
+
<Action.CopyToClipboard title='Copy Email' content={a.email} />
|
|
277
343
|
<Action
|
|
278
|
-
title=
|
|
344
|
+
title='Logout Account'
|
|
279
345
|
icon={Icon.Trash}
|
|
280
346
|
style={Action.Style.Destructive}
|
|
281
347
|
onAction={() => handleRemoved(a.email)}
|
|
@@ -285,12 +351,15 @@ function ManageAccounts({
|
|
|
285
351
|
/>
|
|
286
352
|
))}
|
|
287
353
|
<List.Item
|
|
288
|
-
key=
|
|
289
|
-
title=
|
|
354
|
+
key='add-account'
|
|
355
|
+
title='Add Account'
|
|
290
356
|
icon={Icon.Plus}
|
|
291
357
|
actions={
|
|
292
358
|
<ActionPanel>
|
|
293
|
-
<Action.Push
|
|
359
|
+
<Action.Push
|
|
360
|
+
title='Add Account'
|
|
361
|
+
target={<AddAccount onAdded={handleAdded} />}
|
|
362
|
+
/>
|
|
294
363
|
</ActionPanel>
|
|
295
364
|
}
|
|
296
365
|
/>
|
|
@@ -318,7 +387,10 @@ function ReplyForm({
|
|
|
318
387
|
|
|
319
388
|
const handleSubmit = async (values: { body: string }) => {
|
|
320
389
|
if (!values.body?.trim()) {
|
|
321
|
-
await showToast({
|
|
390
|
+
await showToast({
|
|
391
|
+
style: Toast.Style.Failure,
|
|
392
|
+
title: 'Body is required',
|
|
393
|
+
})
|
|
322
394
|
return
|
|
323
395
|
}
|
|
324
396
|
setIsLoading(true)
|
|
@@ -344,11 +416,15 @@ function ReplyForm({
|
|
|
344
416
|
navigationTitle={replyAll ? 'Reply All' : 'Reply'}
|
|
345
417
|
actions={
|
|
346
418
|
<ActionPanel>
|
|
347
|
-
<Action.SubmitForm title=
|
|
419
|
+
<Action.SubmitForm title='Send Reply' onSubmit={handleSubmit} />
|
|
348
420
|
</ActionPanel>
|
|
349
421
|
}
|
|
350
422
|
>
|
|
351
|
-
<Form.TextArea
|
|
423
|
+
<Form.TextArea
|
|
424
|
+
id='body'
|
|
425
|
+
title='Message'
|
|
426
|
+
placeholder='Type your reply...'
|
|
427
|
+
/>
|
|
352
428
|
</Form>
|
|
353
429
|
)
|
|
354
430
|
}
|
|
@@ -371,11 +447,17 @@ function ForwardForm({
|
|
|
371
447
|
|
|
372
448
|
const handleSubmit = async (values: { to: string; body: string }) => {
|
|
373
449
|
if (!values.to?.trim()) {
|
|
374
|
-
await showToast({
|
|
450
|
+
await showToast({
|
|
451
|
+
style: Toast.Style.Failure,
|
|
452
|
+
title: 'Recipient is required',
|
|
453
|
+
})
|
|
375
454
|
return
|
|
376
455
|
}
|
|
377
456
|
setIsLoading(true)
|
|
378
|
-
const recipients = values.to
|
|
457
|
+
const recipients = values.to
|
|
458
|
+
.split(',')
|
|
459
|
+
.map((e) => ({ email: e.trim() }))
|
|
460
|
+
.filter((e) => e.email)
|
|
379
461
|
const { client } = await getClient([account])
|
|
380
462
|
const result = await client.forwardThread({
|
|
381
463
|
threadId,
|
|
@@ -387,7 +469,10 @@ function ForwardForm({
|
|
|
387
469
|
await showFailureToast(result, { title: 'Failed to forward' })
|
|
388
470
|
return
|
|
389
471
|
}
|
|
390
|
-
await showToast({
|
|
472
|
+
await showToast({
|
|
473
|
+
style: Toast.Style.Success,
|
|
474
|
+
title: `Forwarded to ${values.to}`,
|
|
475
|
+
})
|
|
391
476
|
revalidate()
|
|
392
477
|
pop()
|
|
393
478
|
}
|
|
@@ -395,15 +480,19 @@ function ForwardForm({
|
|
|
395
480
|
return (
|
|
396
481
|
<Form
|
|
397
482
|
isLoading={isLoading}
|
|
398
|
-
navigationTitle=
|
|
483
|
+
navigationTitle='Forward'
|
|
399
484
|
actions={
|
|
400
485
|
<ActionPanel>
|
|
401
|
-
<Action.SubmitForm title=
|
|
486
|
+
<Action.SubmitForm title='Forward' onSubmit={handleSubmit} />
|
|
402
487
|
</ActionPanel>
|
|
403
488
|
}
|
|
404
489
|
>
|
|
405
|
-
<Form.TextField id=
|
|
406
|
-
<Form.TextArea
|
|
490
|
+
<Form.TextField id='to' title='To' placeholder='recipient@example.com' />
|
|
491
|
+
<Form.TextArea
|
|
492
|
+
id='body'
|
|
493
|
+
title='Message'
|
|
494
|
+
placeholder='Optional message to prepend...'
|
|
495
|
+
/>
|
|
407
496
|
</Form>
|
|
408
497
|
)
|
|
409
498
|
}
|
|
@@ -432,7 +521,7 @@ function ThreadDetail({
|
|
|
432
521
|
)
|
|
433
522
|
|
|
434
523
|
if (thread.isLoading || !thread.data) {
|
|
435
|
-
return <Detail markdown=
|
|
524
|
+
return <Detail markdown='' navigationTitle='Loading...' />
|
|
436
525
|
}
|
|
437
526
|
|
|
438
527
|
const t = thread.data
|
|
@@ -457,7 +546,9 @@ function ThreadDetail({
|
|
|
457
546
|
body = renderEmailBody(msg.body, msg.mimeType)
|
|
458
547
|
}
|
|
459
548
|
|
|
460
|
-
return [heading, attachmentLine, '', body]
|
|
549
|
+
return [heading, attachmentLine, '', body]
|
|
550
|
+
.filter((l) => l !== null)
|
|
551
|
+
.join('\n')
|
|
461
552
|
})
|
|
462
553
|
|
|
463
554
|
const markdown = `# ${t.subject}\n\n---\n\n` + parts.join('\n\n---\n\n')
|
|
@@ -470,7 +561,9 @@ function ThreadDetail({
|
|
|
470
561
|
}
|
|
471
562
|
|
|
472
563
|
const labels = [...new Set(messages.flatMap((m) => m.labelIds))]
|
|
473
|
-
.filter(
|
|
564
|
+
.filter(
|
|
565
|
+
(l): l is string => typeof l === 'string' && !l.startsWith('Label_'),
|
|
566
|
+
) // skip internal IDs
|
|
474
567
|
.slice(0, 10)
|
|
475
568
|
|
|
476
569
|
const latestMsg = messages[messages.length - 1]!
|
|
@@ -481,55 +574,96 @@ function ThreadDetail({
|
|
|
481
574
|
markdown={markdown}
|
|
482
575
|
metadata={
|
|
483
576
|
<Detail.Metadata>
|
|
484
|
-
<Detail.Metadata.Label
|
|
485
|
-
|
|
577
|
+
<Detail.Metadata.Label
|
|
578
|
+
title='From'
|
|
579
|
+
text={formatSender(latestMsg.from)}
|
|
580
|
+
/>
|
|
581
|
+
<Detail.Metadata.Label
|
|
582
|
+
title='To'
|
|
583
|
+
text={latestMsg.to.map((r) => r.name || r.email).join(', ')}
|
|
584
|
+
/>
|
|
486
585
|
{latestMsg.cc && latestMsg.cc.length > 0 && (
|
|
487
|
-
<Detail.Metadata.Label
|
|
586
|
+
<Detail.Metadata.Label
|
|
587
|
+
title='Cc'
|
|
588
|
+
text={latestMsg.cc.map((r) => r.name || r.email).join(', ')}
|
|
589
|
+
/>
|
|
488
590
|
)}
|
|
489
|
-
<Detail.Metadata.Label title=
|
|
591
|
+
<Detail.Metadata.Label title='Date' text={latestMsg.date} />
|
|
490
592
|
<Detail.Metadata.Separator />
|
|
491
|
-
<Detail.Metadata.Label
|
|
492
|
-
|
|
593
|
+
<Detail.Metadata.Label
|
|
594
|
+
title='Messages'
|
|
595
|
+
text={String(t.messageCount)}
|
|
596
|
+
/>
|
|
597
|
+
<Detail.Metadata.Label
|
|
598
|
+
title='Participants'
|
|
599
|
+
text={[...participants.values()].join(', ')}
|
|
600
|
+
/>
|
|
493
601
|
{labels.length > 0 && (
|
|
494
|
-
<Detail.Metadata.TagList title=
|
|
602
|
+
<Detail.Metadata.TagList title='Labels'>
|
|
495
603
|
{labels.map((l) => (
|
|
496
|
-
<Detail.Metadata.TagList.Item
|
|
604
|
+
<Detail.Metadata.TagList.Item
|
|
605
|
+
key={l}
|
|
606
|
+
text={l}
|
|
607
|
+
color={labelColor(l)}
|
|
608
|
+
/>
|
|
497
609
|
))}
|
|
498
610
|
</Detail.Metadata.TagList>
|
|
499
611
|
)}
|
|
500
612
|
<Detail.Metadata.Separator />
|
|
501
|
-
<Detail.Metadata.Label title=
|
|
502
|
-
<Detail.Metadata.Label title=
|
|
613
|
+
<Detail.Metadata.Label title='Thread ID' text={t.id} />
|
|
614
|
+
<Detail.Metadata.Label title='Account' text={account} />
|
|
503
615
|
</Detail.Metadata>
|
|
504
616
|
}
|
|
505
617
|
actions={
|
|
506
618
|
<ActionPanel>
|
|
507
|
-
<ActionPanel.Section title=
|
|
619
|
+
<ActionPanel.Section title='Reply & Forward'>
|
|
508
620
|
<Action.Push
|
|
509
|
-
title=
|
|
621
|
+
title='Reply'
|
|
510
622
|
icon={Icon.Reply}
|
|
511
623
|
shortcut={{ modifiers: ['ctrl'], key: 'r' }}
|
|
512
|
-
target={
|
|
624
|
+
target={
|
|
625
|
+
<ReplyForm
|
|
626
|
+
threadId={threadId}
|
|
627
|
+
account={account}
|
|
628
|
+
revalidate={revalidate}
|
|
629
|
+
/>
|
|
630
|
+
}
|
|
513
631
|
/>
|
|
514
632
|
<Action.Push
|
|
515
|
-
title=
|
|
633
|
+
title='Reply All'
|
|
516
634
|
icon={Icon.Reply}
|
|
517
|
-
shortcut={{
|
|
518
|
-
|
|
635
|
+
shortcut={{
|
|
636
|
+
modifiers: ['ctrl', 'shift'],
|
|
637
|
+
key: 'r',
|
|
638
|
+
}}
|
|
639
|
+
target={
|
|
640
|
+
<ReplyForm
|
|
641
|
+
threadId={threadId}
|
|
642
|
+
account={account}
|
|
643
|
+
replyAll
|
|
644
|
+
revalidate={revalidate}
|
|
645
|
+
/>
|
|
646
|
+
}
|
|
519
647
|
/>
|
|
520
648
|
<Action.Push
|
|
521
|
-
title=
|
|
649
|
+
title='Forward'
|
|
522
650
|
icon={Icon.Forward}
|
|
523
651
|
shortcut={{ modifiers: ['ctrl'], key: 'f' }}
|
|
524
|
-
target={
|
|
652
|
+
target={
|
|
653
|
+
<ForwardForm
|
|
654
|
+
threadId={threadId}
|
|
655
|
+
account={account}
|
|
656
|
+
revalidate={revalidate}
|
|
657
|
+
/>
|
|
658
|
+
}
|
|
525
659
|
/>
|
|
526
660
|
</ActionPanel.Section>
|
|
527
|
-
<ActionPanel.Section title=
|
|
528
|
-
<Action.CopyToClipboard title=
|
|
529
|
-
<Action.CopyToClipboard title=
|
|
661
|
+
<ActionPanel.Section title='Copy'>
|
|
662
|
+
<Action.CopyToClipboard title='Copy Thread ID' content={t.id} />
|
|
663
|
+
<Action.CopyToClipboard title='Copy Subject' content={t.subject} />
|
|
530
664
|
{latestMsg && (
|
|
531
665
|
<Action.CopyToClipboard
|
|
532
|
-
title=
|
|
666
|
+
title='Copy Email Body'
|
|
533
667
|
content={renderEmailBody(latestMsg.body, latestMsg.mimeType)}
|
|
534
668
|
/>
|
|
535
669
|
)}
|
|
@@ -569,7 +703,8 @@ export default function Command() {
|
|
|
569
703
|
|
|
570
704
|
// Single selected account: standard cursor pagination.
|
|
571
705
|
if (account !== 'all') {
|
|
572
|
-
const pageToken =
|
|
706
|
+
const pageToken =
|
|
707
|
+
cursor?.mode === 'single' ? cursor.nextPageToken : undefined
|
|
573
708
|
const { email, client } = clients[0]!
|
|
574
709
|
const result = await client.listThreads({
|
|
575
710
|
query: query || undefined,
|
|
@@ -577,10 +712,15 @@ export default function Command() {
|
|
|
577
712
|
pageToken: pageToken || undefined,
|
|
578
713
|
})
|
|
579
714
|
if (result instanceof Error) {
|
|
580
|
-
await showFailureToast(result, {
|
|
715
|
+
await showFailureToast(result, {
|
|
716
|
+
title: 'Failed to fetch emails',
|
|
717
|
+
})
|
|
581
718
|
return { data: [] as ThreadItem[], hasMore: false }
|
|
582
719
|
}
|
|
583
|
-
const data: ThreadItem[] = result.threads.map((t) => ({
|
|
720
|
+
const data: ThreadItem[] = result.threads.map((t) => ({
|
|
721
|
+
...t,
|
|
722
|
+
account: email,
|
|
723
|
+
}))
|
|
584
724
|
return {
|
|
585
725
|
data,
|
|
586
726
|
hasMore: !!result.nextPageToken,
|
|
@@ -592,7 +732,8 @@ export default function Command() {
|
|
|
592
732
|
}
|
|
593
733
|
|
|
594
734
|
// Multi-account: keep one token per account and merge sorted pages.
|
|
595
|
-
const previousByAccount =
|
|
735
|
+
const previousByAccount =
|
|
736
|
+
cursor?.mode === 'multi' ? cursor.nextByAccount : {}
|
|
596
737
|
|
|
597
738
|
const results = await Promise.all(
|
|
598
739
|
clients.map(async ({ email, client }) => {
|
|
@@ -630,20 +771,29 @@ export default function Command() {
|
|
|
630
771
|
.filter(isTruthy)
|
|
631
772
|
|
|
632
773
|
const merged: ThreadItem[] = successfulResults
|
|
633
|
-
.flatMap(({ email, result }) =>
|
|
634
|
-
|
|
774
|
+
.flatMap(({ email, result }) =>
|
|
775
|
+
result.threads.map((t) => ({ ...t, account: email })),
|
|
776
|
+
)
|
|
777
|
+
.sort(
|
|
778
|
+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
|
779
|
+
)
|
|
635
780
|
|
|
636
781
|
const nextByAccount: Record<string, string | null> = {}
|
|
637
782
|
for (const { email, nextPageToken } of results) {
|
|
638
783
|
nextByAccount[email] = nextPageToken
|
|
639
784
|
}
|
|
640
785
|
|
|
641
|
-
const hasMore = Object.values(nextByAccount).some(
|
|
786
|
+
const hasMore = Object.values(nextByAccount).some(
|
|
787
|
+
(token) => token !== null,
|
|
788
|
+
)
|
|
642
789
|
|
|
643
790
|
return {
|
|
644
791
|
data: merged,
|
|
645
792
|
hasMore,
|
|
646
|
-
cursor: {
|
|
793
|
+
cursor: {
|
|
794
|
+
mode: 'multi',
|
|
795
|
+
nextByAccount,
|
|
796
|
+
} satisfies MailCursor,
|
|
647
797
|
}
|
|
648
798
|
}
|
|
649
799
|
},
|
|
@@ -695,7 +845,9 @@ export default function Command() {
|
|
|
695
845
|
// Selection helpers
|
|
696
846
|
const toggleSelection = useCallback((threadId: string) => {
|
|
697
847
|
setSelectedThreads((prev) =>
|
|
698
|
-
prev.includes(threadId)
|
|
848
|
+
prev.includes(threadId)
|
|
849
|
+
? prev.filter((id) => id !== threadId)
|
|
850
|
+
: [...prev, threadId],
|
|
699
851
|
)
|
|
700
852
|
}, [])
|
|
701
853
|
|
|
@@ -721,12 +873,17 @@ export default function Command() {
|
|
|
721
873
|
const { client } = await getClient([acct])
|
|
722
874
|
const result = await fn(client, ids)
|
|
723
875
|
if (result instanceof Error) {
|
|
724
|
-
await showFailureToast(result, {
|
|
876
|
+
await showFailureToast(result, {
|
|
877
|
+
title: `Failed to ${actionName}`,
|
|
878
|
+
})
|
|
725
879
|
return
|
|
726
880
|
}
|
|
727
881
|
}
|
|
728
882
|
|
|
729
|
-
await showToast({
|
|
883
|
+
await showToast({
|
|
884
|
+
style: Toast.Style.Success,
|
|
885
|
+
title: `${actionName}: ${selectedThreads.length} thread(s)`,
|
|
886
|
+
})
|
|
730
887
|
setSelectedThreads([])
|
|
731
888
|
revalidate()
|
|
732
889
|
},
|
|
@@ -737,7 +894,7 @@ export default function Command() {
|
|
|
737
894
|
<List
|
|
738
895
|
isLoading={isLoading || accounts.isLoading}
|
|
739
896
|
isShowingDetail={isShowingDetail}
|
|
740
|
-
searchBarPlaceholder=
|
|
897
|
+
searchBarPlaceholder='Search emails...'
|
|
741
898
|
onSearchTextChange={setSearchText}
|
|
742
899
|
throttle
|
|
743
900
|
pagination={pagination ? { ...pagination, pageSize } : undefined}
|
|
@@ -761,16 +918,33 @@ export default function Command() {
|
|
|
761
918
|
|
|
762
919
|
// Icon: selection mode or status
|
|
763
920
|
const icon = hasSelection
|
|
764
|
-
? {
|
|
921
|
+
? {
|
|
922
|
+
source: isSelected ? Icon.CheckCircle : Icon.Circle,
|
|
923
|
+
tintColor: isSelected ? Color.Blue : Color.SecondaryText,
|
|
924
|
+
}
|
|
765
925
|
: threadStatusIcon(thread)
|
|
766
926
|
|
|
767
927
|
// Accessories
|
|
768
|
-
const accessories: Array<{
|
|
928
|
+
const accessories: Array<{
|
|
929
|
+
text?: string
|
|
930
|
+
tag?: string | { value: string; color?: string }
|
|
931
|
+
icon?: string | null
|
|
932
|
+
}> = []
|
|
769
933
|
if (thread.messageCount > 1) {
|
|
770
|
-
accessories.push({
|
|
934
|
+
accessories.push({
|
|
935
|
+
tag: {
|
|
936
|
+
value: String(thread.messageCount),
|
|
937
|
+
color: Color.SecondaryText,
|
|
938
|
+
},
|
|
939
|
+
})
|
|
771
940
|
}
|
|
772
941
|
if (multiAccount || selectedAccount === 'all') {
|
|
773
|
-
accessories.push({
|
|
942
|
+
accessories.push({
|
|
943
|
+
tag: {
|
|
944
|
+
value: thread.account.split('@')[0] ?? thread.account,
|
|
945
|
+
color: accountColor(thread.account),
|
|
946
|
+
},
|
|
947
|
+
})
|
|
774
948
|
}
|
|
775
949
|
accessories.push({ text: formatDate(thread.date) })
|
|
776
950
|
|
|
@@ -780,24 +954,43 @@ export default function Command() {
|
|
|
780
954
|
markdown={`# ${thread.subject}\n\n${thread.snippet}`}
|
|
781
955
|
metadata={
|
|
782
956
|
<List.Item.Detail.Metadata>
|
|
783
|
-
<List.Item.Detail.Metadata.Label
|
|
784
|
-
|
|
957
|
+
<List.Item.Detail.Metadata.Label
|
|
958
|
+
title='From'
|
|
959
|
+
text={formatSender(thread.from)}
|
|
960
|
+
/>
|
|
961
|
+
<List.Item.Detail.Metadata.Label
|
|
962
|
+
title='Date'
|
|
963
|
+
text={thread.date}
|
|
964
|
+
/>
|
|
785
965
|
<List.Item.Detail.Metadata.Separator />
|
|
786
966
|
{thread.labelIds.length > 0 && (
|
|
787
|
-
<List.Item.Detail.Metadata.TagList title=
|
|
967
|
+
<List.Item.Detail.Metadata.TagList title='Labels'>
|
|
788
968
|
{thread.labelIds
|
|
789
969
|
.filter((l) => !l.startsWith('Label_'))
|
|
790
970
|
.slice(0, 8)
|
|
791
971
|
.map((l) => (
|
|
792
|
-
<List.Item.Detail.Metadata.TagList.Item
|
|
972
|
+
<List.Item.Detail.Metadata.TagList.Item
|
|
973
|
+
key={l}
|
|
974
|
+
text={l}
|
|
975
|
+
color={labelColor(l)}
|
|
976
|
+
/>
|
|
793
977
|
))}
|
|
794
978
|
</List.Item.Detail.Metadata.TagList>
|
|
795
979
|
)}
|
|
796
980
|
<List.Item.Detail.Metadata.Separator />
|
|
797
|
-
<List.Item.Detail.Metadata.Label
|
|
798
|
-
|
|
981
|
+
<List.Item.Detail.Metadata.Label
|
|
982
|
+
title='Messages'
|
|
983
|
+
text={String(thread.messageCount)}
|
|
984
|
+
/>
|
|
985
|
+
<List.Item.Detail.Metadata.Label
|
|
986
|
+
title='Thread ID'
|
|
987
|
+
text={thread.id}
|
|
988
|
+
/>
|
|
799
989
|
{multiAccount && (
|
|
800
|
-
<List.Item.Detail.Metadata.Label
|
|
990
|
+
<List.Item.Detail.Metadata.Label
|
|
991
|
+
title='Account'
|
|
992
|
+
text={thread.account}
|
|
993
|
+
/>
|
|
801
994
|
)}
|
|
802
995
|
</List.Item.Detail.Metadata>
|
|
803
996
|
}
|
|
@@ -811,15 +1004,21 @@ export default function Command() {
|
|
|
811
1004
|
subtitle={formatSender(thread.from)}
|
|
812
1005
|
icon={icon}
|
|
813
1006
|
accessories={accessories}
|
|
814
|
-
keywords={[
|
|
1007
|
+
keywords={[
|
|
1008
|
+
thread.from.email,
|
|
1009
|
+
thread.from.name ?? '',
|
|
1010
|
+
thread.account,
|
|
1011
|
+
]}
|
|
815
1012
|
detail={detail}
|
|
816
1013
|
actions={
|
|
817
1014
|
<ActionPanel>
|
|
818
1015
|
{/* Selection actions (when items are selected) */}
|
|
819
1016
|
{hasSelection && (
|
|
820
|
-
<ActionPanel.Section title=
|
|
1017
|
+
<ActionPanel.Section title='Selection'>
|
|
821
1018
|
<Action
|
|
822
|
-
title={
|
|
1019
|
+
title={
|
|
1020
|
+
isSelected ? 'Deselect Thread' : 'Select Thread'
|
|
1021
|
+
}
|
|
823
1022
|
icon={isSelected ? Icon.CheckCircle : Icon.Circle}
|
|
824
1023
|
onAction={() => toggleSelection(thread.id)}
|
|
825
1024
|
/>
|
|
@@ -827,21 +1026,33 @@ export default function Command() {
|
|
|
827
1026
|
title={`Archive ${selectedThreads.length} Selected`}
|
|
828
1027
|
icon={Icon.Tray}
|
|
829
1028
|
onAction={() =>
|
|
830
|
-
handleBulkAction('Archived', (c, ids) =>
|
|
1029
|
+
handleBulkAction('Archived', (c, ids) =>
|
|
1030
|
+
c.archive({
|
|
1031
|
+
threadIds: ids,
|
|
1032
|
+
}),
|
|
1033
|
+
)
|
|
831
1034
|
}
|
|
832
1035
|
/>
|
|
833
1036
|
<Action
|
|
834
1037
|
title={`Mark ${selectedThreads.length} as Read`}
|
|
835
1038
|
icon={Icon.Eye}
|
|
836
1039
|
onAction={() =>
|
|
837
|
-
handleBulkAction('Marked as read', (c, ids) =>
|
|
1040
|
+
handleBulkAction('Marked as read', (c, ids) =>
|
|
1041
|
+
c.markAsRead({
|
|
1042
|
+
threadIds: ids,
|
|
1043
|
+
}),
|
|
1044
|
+
)
|
|
838
1045
|
}
|
|
839
1046
|
/>
|
|
840
1047
|
<Action
|
|
841
1048
|
title={`Star ${selectedThreads.length} Selected`}
|
|
842
1049
|
icon={Icon.Star}
|
|
843
1050
|
onAction={() =>
|
|
844
|
-
handleBulkAction('Starred', (c, ids) =>
|
|
1051
|
+
handleBulkAction('Starred', (c, ids) =>
|
|
1052
|
+
c.star({
|
|
1053
|
+
threadIds: ids,
|
|
1054
|
+
}),
|
|
1055
|
+
)
|
|
845
1056
|
}
|
|
846
1057
|
/>
|
|
847
1058
|
<Action
|
|
@@ -851,13 +1062,15 @@ export default function Command() {
|
|
|
851
1062
|
onAction={() =>
|
|
852
1063
|
handleBulkAction('Trashed', async (c, ids) => {
|
|
853
1064
|
for (const id of ids) {
|
|
854
|
-
await c.trash({
|
|
1065
|
+
await c.trash({
|
|
1066
|
+
threadId: id,
|
|
1067
|
+
})
|
|
855
1068
|
}
|
|
856
1069
|
})
|
|
857
1070
|
}
|
|
858
1071
|
/>
|
|
859
1072
|
<Action
|
|
860
|
-
title=
|
|
1073
|
+
title='Deselect All'
|
|
861
1074
|
icon={Icon.XMarkCircle}
|
|
862
1075
|
onAction={() => setSelectedThreads([])}
|
|
863
1076
|
/>
|
|
@@ -867,7 +1080,7 @@ export default function Command() {
|
|
|
867
1080
|
{/* Primary actions */}
|
|
868
1081
|
<ActionPanel.Section>
|
|
869
1082
|
<Action.Push
|
|
870
|
-
title=
|
|
1083
|
+
title='Open Thread'
|
|
871
1084
|
icon={Icon.Eye}
|
|
872
1085
|
target={
|
|
873
1086
|
<ThreadDetail
|
|
@@ -879,57 +1092,90 @@ export default function Command() {
|
|
|
879
1092
|
/>
|
|
880
1093
|
{!hasSelection && (
|
|
881
1094
|
<Action
|
|
882
|
-
title=
|
|
1095
|
+
title='Select Thread'
|
|
883
1096
|
icon={Icon.CheckCircle}
|
|
884
|
-
shortcut={{
|
|
1097
|
+
shortcut={{
|
|
1098
|
+
modifiers: ['ctrl'],
|
|
1099
|
+
key: 'x',
|
|
1100
|
+
}}
|
|
885
1101
|
onAction={() => toggleSelection(thread.id)}
|
|
886
1102
|
/>
|
|
887
1103
|
)}
|
|
888
1104
|
<Action
|
|
889
|
-
title={
|
|
1105
|
+
title={
|
|
1106
|
+
thread.unread ? 'Mark as Read' : 'Mark as Unread'
|
|
1107
|
+
}
|
|
890
1108
|
icon={thread.unread ? Icon.Eye : Icon.EyeDisabled}
|
|
891
|
-
shortcut={{
|
|
1109
|
+
shortcut={{
|
|
1110
|
+
modifiers: ['ctrl'],
|
|
1111
|
+
key: 'u',
|
|
1112
|
+
}}
|
|
892
1113
|
onAction={async () => {
|
|
893
1114
|
const { client } = await getClient([thread.account])
|
|
894
1115
|
const result = thread.unread
|
|
895
|
-
? await client.markAsRead({
|
|
896
|
-
|
|
1116
|
+
? await client.markAsRead({
|
|
1117
|
+
threadIds: [thread.id],
|
|
1118
|
+
})
|
|
1119
|
+
: await client.markAsUnread({
|
|
1120
|
+
threadIds: [thread.id],
|
|
1121
|
+
})
|
|
897
1122
|
if (result instanceof Error) {
|
|
898
1123
|
await showFailureToast(result)
|
|
899
1124
|
return
|
|
900
1125
|
}
|
|
901
1126
|
await showToast({
|
|
902
1127
|
style: Toast.Style.Success,
|
|
903
|
-
title: thread.unread
|
|
1128
|
+
title: thread.unread
|
|
1129
|
+
? 'Marked as read'
|
|
1130
|
+
: 'Marked as unread',
|
|
904
1131
|
})
|
|
905
1132
|
revalidate()
|
|
906
1133
|
}}
|
|
907
1134
|
/>
|
|
908
1135
|
<Action
|
|
909
|
-
title=
|
|
1136
|
+
title='Archive'
|
|
910
1137
|
icon={Icon.Tray}
|
|
911
|
-
shortcut={{
|
|
1138
|
+
shortcut={{
|
|
1139
|
+
modifiers: ['ctrl'],
|
|
1140
|
+
key: 'e',
|
|
1141
|
+
}}
|
|
912
1142
|
onAction={async () => {
|
|
913
1143
|
const { client } = await getClient([thread.account])
|
|
914
|
-
const result = await client.archive({
|
|
1144
|
+
const result = await client.archive({
|
|
1145
|
+
threadIds: [thread.id],
|
|
1146
|
+
})
|
|
915
1147
|
if (result instanceof Error) {
|
|
916
1148
|
await showFailureToast(result)
|
|
917
1149
|
return
|
|
918
1150
|
}
|
|
919
|
-
await showToast({
|
|
1151
|
+
await showToast({
|
|
1152
|
+
style: Toast.Style.Success,
|
|
1153
|
+
title: 'Archived',
|
|
1154
|
+
})
|
|
920
1155
|
revalidate()
|
|
921
1156
|
}}
|
|
922
1157
|
/>
|
|
923
1158
|
<Action
|
|
924
|
-
title={
|
|
1159
|
+
title={
|
|
1160
|
+
thread.labelIds.includes('STARRED')
|
|
1161
|
+
? 'Unstar'
|
|
1162
|
+
: 'Star'
|
|
1163
|
+
}
|
|
925
1164
|
icon={Icon.Star}
|
|
926
|
-
shortcut={{
|
|
1165
|
+
shortcut={{
|
|
1166
|
+
modifiers: ['ctrl'],
|
|
1167
|
+
key: 's',
|
|
1168
|
+
}}
|
|
927
1169
|
onAction={async () => {
|
|
928
1170
|
const { client } = await getClient([thread.account])
|
|
929
1171
|
const isStarred = thread.labelIds.includes('STARRED')
|
|
930
1172
|
const result = isStarred
|
|
931
|
-
? await client.unstar({
|
|
932
|
-
|
|
1173
|
+
? await client.unstar({
|
|
1174
|
+
threadIds: [thread.id],
|
|
1175
|
+
})
|
|
1176
|
+
: await client.star({
|
|
1177
|
+
threadIds: [thread.id],
|
|
1178
|
+
})
|
|
933
1179
|
if (result instanceof Error) {
|
|
934
1180
|
await showFailureToast(result)
|
|
935
1181
|
return
|
|
@@ -944,11 +1190,14 @@ export default function Command() {
|
|
|
944
1190
|
</ActionPanel.Section>
|
|
945
1191
|
|
|
946
1192
|
{/* Reply & Forward */}
|
|
947
|
-
<ActionPanel.Section title=
|
|
1193
|
+
<ActionPanel.Section title='Reply & Forward'>
|
|
948
1194
|
<Action.Push
|
|
949
|
-
title=
|
|
1195
|
+
title='Reply'
|
|
950
1196
|
icon={Icon.Reply}
|
|
951
|
-
shortcut={{
|
|
1197
|
+
shortcut={{
|
|
1198
|
+
modifiers: ['ctrl'],
|
|
1199
|
+
key: 'r',
|
|
1200
|
+
}}
|
|
952
1201
|
target={
|
|
953
1202
|
<ReplyForm
|
|
954
1203
|
threadId={thread.id}
|
|
@@ -958,9 +1207,12 @@ export default function Command() {
|
|
|
958
1207
|
}
|
|
959
1208
|
/>
|
|
960
1209
|
<Action.Push
|
|
961
|
-
title=
|
|
1210
|
+
title='Reply All'
|
|
962
1211
|
icon={Icon.Reply}
|
|
963
|
-
shortcut={{
|
|
1212
|
+
shortcut={{
|
|
1213
|
+
modifiers: ['ctrl', 'shift'],
|
|
1214
|
+
key: 'r',
|
|
1215
|
+
}}
|
|
964
1216
|
target={
|
|
965
1217
|
<ReplyForm
|
|
966
1218
|
threadId={thread.id}
|
|
@@ -971,9 +1223,12 @@ export default function Command() {
|
|
|
971
1223
|
}
|
|
972
1224
|
/>
|
|
973
1225
|
<Action.Push
|
|
974
|
-
title=
|
|
1226
|
+
title='Forward'
|
|
975
1227
|
icon={Icon.Forward}
|
|
976
|
-
shortcut={{
|
|
1228
|
+
shortcut={{
|
|
1229
|
+
modifiers: ['ctrl'],
|
|
1230
|
+
key: 'f',
|
|
1231
|
+
}}
|
|
977
1232
|
target={
|
|
978
1233
|
<ForwardForm
|
|
979
1234
|
threadId={thread.id}
|
|
@@ -985,23 +1240,40 @@ export default function Command() {
|
|
|
985
1240
|
</ActionPanel.Section>
|
|
986
1241
|
|
|
987
1242
|
{/* Copy */}
|
|
988
|
-
<ActionPanel.Section title=
|
|
989
|
-
<Action.CopyToClipboard
|
|
990
|
-
|
|
991
|
-
|
|
1243
|
+
<ActionPanel.Section title='Copy'>
|
|
1244
|
+
<Action.CopyToClipboard
|
|
1245
|
+
title='Copy Thread ID'
|
|
1246
|
+
content={thread.id}
|
|
1247
|
+
/>
|
|
1248
|
+
<Action.CopyToClipboard
|
|
1249
|
+
title='Copy Subject'
|
|
1250
|
+
content={thread.subject}
|
|
1251
|
+
/>
|
|
1252
|
+
<Action.CopyToClipboard
|
|
1253
|
+
title='Copy Sender Email'
|
|
1254
|
+
content={thread.from.email}
|
|
1255
|
+
/>
|
|
992
1256
|
</ActionPanel.Section>
|
|
993
1257
|
|
|
994
1258
|
{/* Danger */}
|
|
995
1259
|
<ActionPanel.Section>
|
|
996
1260
|
<Action
|
|
997
|
-
title=
|
|
1261
|
+
title='Trash'
|
|
998
1262
|
icon={Icon.Trash}
|
|
999
1263
|
style={Action.Style.Destructive}
|
|
1000
|
-
shortcut={{
|
|
1264
|
+
shortcut={{
|
|
1265
|
+
modifiers: ['ctrl'],
|
|
1266
|
+
key: 'backspace',
|
|
1267
|
+
}}
|
|
1001
1268
|
onAction={async () => {
|
|
1002
1269
|
const { client } = await getClient([thread.account])
|
|
1003
|
-
await client.trash({
|
|
1004
|
-
|
|
1270
|
+
await client.trash({
|
|
1271
|
+
threadId: thread.id,
|
|
1272
|
+
})
|
|
1273
|
+
await showToast({
|
|
1274
|
+
style: Toast.Style.Success,
|
|
1275
|
+
title: 'Trashed',
|
|
1276
|
+
})
|
|
1005
1277
|
revalidate()
|
|
1006
1278
|
}}
|
|
1007
1279
|
/>
|
|
@@ -1010,15 +1282,21 @@ export default function Command() {
|
|
|
1010
1282
|
{/* Utility */}
|
|
1011
1283
|
<ActionPanel.Section>
|
|
1012
1284
|
<Action
|
|
1013
|
-
title=
|
|
1285
|
+
title='Refresh'
|
|
1014
1286
|
icon={Icon.ArrowClockwise}
|
|
1015
|
-
shortcut={{
|
|
1287
|
+
shortcut={{
|
|
1288
|
+
modifiers: ['ctrl', 'shift'],
|
|
1289
|
+
key: 'r',
|
|
1290
|
+
}}
|
|
1016
1291
|
onAction={() => revalidate()}
|
|
1017
1292
|
/>
|
|
1018
1293
|
<Action
|
|
1019
|
-
title=
|
|
1294
|
+
title='Toggle Detail'
|
|
1020
1295
|
icon={Icon.Sidebar}
|
|
1021
|
-
shortcut={{
|
|
1296
|
+
shortcut={{
|
|
1297
|
+
modifiers: ['ctrl'],
|
|
1298
|
+
key: 'd',
|
|
1299
|
+
}}
|
|
1022
1300
|
onAction={() => setIsShowingDetail((v) => !v)}
|
|
1023
1301
|
/>
|
|
1024
1302
|
</ActionPanel.Section>
|