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/src/mail-tui.tsx
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
showFailureToast,
|
|
36
36
|
} from 'termcast'
|
|
37
37
|
import { useTerminalDimensions } from '@opentui/react'
|
|
38
|
-
import { useCachedPromise } from '@termcast/utils'
|
|
38
|
+
import { useCachedPromise, useCachedState } from '@termcast/utils'
|
|
39
39
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
40
40
|
|
|
41
41
|
import {
|
|
@@ -63,6 +63,10 @@ const DEFAULT_PAGE_SIZE = 25
|
|
|
63
63
|
const MIN_PAGE_SIZE = 10
|
|
64
64
|
const VISIBLE_ROWS_OFFSET = 6
|
|
65
65
|
|
|
66
|
+
/** Spacing mode for the mail list. 'relaxed' renders each item as 3 lines. */
|
|
67
|
+
const LIST_SPACING_MODE: 'default' | 'relaxed' = 'relaxed'
|
|
68
|
+
const LINES_PER_ITEM = LIST_SPACING_MODE === 'relaxed' ? 3 : 1
|
|
69
|
+
|
|
66
70
|
const ACCOUNT_COLORS = [
|
|
67
71
|
Color.Blue,
|
|
68
72
|
Color.Green,
|
|
@@ -140,7 +144,9 @@ type MailCursor =
|
|
|
140
144
|
|
|
141
145
|
function getPageSizeFromTerminalHeight(rows?: number): number {
|
|
142
146
|
if (typeof rows !== 'number' || rows <= 0) return DEFAULT_PAGE_SIZE
|
|
143
|
-
|
|
147
|
+
const visibleRows = rows - VISIBLE_ROWS_OFFSET
|
|
148
|
+
const itemCount = Math.floor(visibleRows / LINES_PER_ITEM)
|
|
149
|
+
return Math.max(MIN_PAGE_SIZE, itemCount)
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
// ---------------------------------------------------------------------------
|
|
@@ -203,7 +209,7 @@ function AccountDropdown({
|
|
|
203
209
|
/>
|
|
204
210
|
))}
|
|
205
211
|
</List.Dropdown.Section>
|
|
206
|
-
<List.Dropdown.Section>
|
|
212
|
+
<List.Dropdown.Section title='Manage Accounts'>
|
|
207
213
|
<List.Dropdown.Item
|
|
208
214
|
title='Add Account'
|
|
209
215
|
value={ADD_ACCOUNT}
|
|
@@ -343,7 +349,6 @@ function ManageAccounts({
|
|
|
343
349
|
<Action
|
|
344
350
|
title='Logout Account'
|
|
345
351
|
icon={Icon.Trash}
|
|
346
|
-
style={Action.Style.Destructive}
|
|
347
352
|
onAction={() => handleRemoved(a.email)}
|
|
348
353
|
/>
|
|
349
354
|
</ActionPanel>
|
|
@@ -368,130 +373,134 @@ function ManageAccounts({
|
|
|
368
373
|
}
|
|
369
374
|
|
|
370
375
|
// ---------------------------------------------------------------------------
|
|
371
|
-
//
|
|
376
|
+
// Compose Form (unified: reply, reply all, forward)
|
|
372
377
|
// ---------------------------------------------------------------------------
|
|
373
378
|
|
|
374
|
-
|
|
375
|
-
threadId
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
379
|
+
type ComposeMode =
|
|
380
|
+
| { type: 'reply'; threadId: string; replyAll?: boolean }
|
|
381
|
+
| { type: 'forward'; threadId: string }
|
|
382
|
+
|
|
383
|
+
interface ComposeFormProps {
|
|
384
|
+
mode: ComposeMode
|
|
385
|
+
initialAccount: string
|
|
386
|
+
accounts: AuthStatus[]
|
|
387
|
+
onSent?: () => void
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function ComposeForm({ mode, initialAccount, accounts, onSent }: ComposeFormProps) {
|
|
385
391
|
const { pop } = useNavigation()
|
|
386
392
|
const [isLoading, setIsLoading] = useState(false)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
const [selectedAccount, setSelectedAccount] = useState(initialAccount)
|
|
394
|
+
|
|
395
|
+
const navigationTitle =
|
|
396
|
+
mode.type === 'forward'
|
|
397
|
+
? 'Forward'
|
|
398
|
+
: mode.replyAll
|
|
399
|
+
? 'Reply All'
|
|
400
|
+
: 'Reply'
|
|
401
|
+
|
|
402
|
+
const bodyPlaceholder =
|
|
403
|
+
mode.type === 'forward'
|
|
404
|
+
? 'Add a message (optional)...'
|
|
405
|
+
: 'Type your reply...'
|
|
406
|
+
|
|
407
|
+
const handleSubmit = async (values: { to?: string; body?: string }) => {
|
|
408
|
+
// Validate based on mode
|
|
409
|
+
if (mode.type === 'forward' && !values.to?.trim()) {
|
|
390
410
|
await showToast({
|
|
391
411
|
style: Toast.Style.Failure,
|
|
392
|
-
title: '
|
|
412
|
+
title: 'Recipient is required',
|
|
393
413
|
})
|
|
394
414
|
return
|
|
395
415
|
}
|
|
396
|
-
|
|
397
|
-
const { client } = await getClient([account])
|
|
398
|
-
const result = await client.replyToThread({
|
|
399
|
-
threadId,
|
|
400
|
-
body: values.body,
|
|
401
|
-
replyAll,
|
|
402
|
-
})
|
|
403
|
-
setIsLoading(false)
|
|
404
|
-
if (result instanceof Error) {
|
|
405
|
-
await showFailureToast(result, { title: 'Failed to send reply' })
|
|
406
|
-
return
|
|
407
|
-
}
|
|
408
|
-
await showToast({ style: Toast.Style.Success, title: 'Reply sent' })
|
|
409
|
-
revalidate()
|
|
410
|
-
pop()
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return (
|
|
414
|
-
<Form
|
|
415
|
-
isLoading={isLoading}
|
|
416
|
-
navigationTitle={replyAll ? 'Reply All' : 'Reply'}
|
|
417
|
-
actions={
|
|
418
|
-
<ActionPanel>
|
|
419
|
-
<Action.SubmitForm title='Send Reply' onSubmit={handleSubmit} />
|
|
420
|
-
</ActionPanel>
|
|
421
|
-
}
|
|
422
|
-
>
|
|
423
|
-
<Form.TextArea
|
|
424
|
-
id='body'
|
|
425
|
-
title='Message'
|
|
426
|
-
placeholder='Type your reply...'
|
|
427
|
-
/>
|
|
428
|
-
</Form>
|
|
429
|
-
)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// ---------------------------------------------------------------------------
|
|
433
|
-
// Forward Form
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
|
|
436
|
-
function ForwardForm({
|
|
437
|
-
threadId,
|
|
438
|
-
account,
|
|
439
|
-
revalidate,
|
|
440
|
-
}: {
|
|
441
|
-
threadId: string
|
|
442
|
-
account: string
|
|
443
|
-
revalidate: () => void
|
|
444
|
-
}) {
|
|
445
|
-
const { pop } = useNavigation()
|
|
446
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
447
|
-
|
|
448
|
-
const handleSubmit = async (values: { to: string; body: string }) => {
|
|
449
|
-
if (!values.to?.trim()) {
|
|
416
|
+
if (mode.type !== 'forward' && !values.body?.trim()) {
|
|
450
417
|
await showToast({
|
|
451
418
|
style: Toast.Style.Failure,
|
|
452
|
-
title: '
|
|
419
|
+
title: 'Message is required',
|
|
453
420
|
})
|
|
454
421
|
return
|
|
455
422
|
}
|
|
423
|
+
|
|
456
424
|
setIsLoading(true)
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
425
|
+
const { client } = await getClient([selectedAccount])
|
|
426
|
+
|
|
427
|
+
let result: Error | unknown
|
|
428
|
+
if (mode.type === 'forward') {
|
|
429
|
+
const recipients = (values.to ?? '')
|
|
430
|
+
.split(',')
|
|
431
|
+
.map((e) => ({ email: e.trim() }))
|
|
432
|
+
.filter((e) => e.email)
|
|
433
|
+
result = await client.forwardThread({
|
|
434
|
+
threadId: mode.threadId,
|
|
435
|
+
to: recipients,
|
|
436
|
+
body: values.body || undefined,
|
|
437
|
+
})
|
|
438
|
+
} else {
|
|
439
|
+
result = await client.replyToThread({
|
|
440
|
+
threadId: mode.threadId,
|
|
441
|
+
body: values.body ?? '',
|
|
442
|
+
replyAll: mode.replyAll,
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
467
446
|
setIsLoading(false)
|
|
447
|
+
|
|
468
448
|
if (result instanceof Error) {
|
|
469
|
-
await showFailureToast(result, {
|
|
449
|
+
await showFailureToast(result, {
|
|
450
|
+
title: mode.type === 'forward' ? 'Failed to forward' : 'Failed to send reply',
|
|
451
|
+
})
|
|
470
452
|
return
|
|
471
453
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
454
|
+
|
|
455
|
+
const successTitle =
|
|
456
|
+
mode.type === 'forward'
|
|
457
|
+
? `Forwarded to ${values.to}`
|
|
458
|
+
: 'Reply sent'
|
|
459
|
+
await showToast({ style: Toast.Style.Success, title: successTitle })
|
|
460
|
+
onSent?.()
|
|
477
461
|
pop()
|
|
478
462
|
}
|
|
479
463
|
|
|
480
464
|
return (
|
|
481
465
|
<Form
|
|
482
466
|
isLoading={isLoading}
|
|
483
|
-
navigationTitle=
|
|
467
|
+
navigationTitle={navigationTitle}
|
|
484
468
|
actions={
|
|
485
469
|
<ActionPanel>
|
|
486
|
-
<Action.SubmitForm
|
|
470
|
+
<Action.SubmitForm
|
|
471
|
+
title={mode.type === 'forward' ? 'Forward' : 'Send Reply'}
|
|
472
|
+
onSubmit={handleSubmit}
|
|
473
|
+
/>
|
|
487
474
|
</ActionPanel>
|
|
488
475
|
}
|
|
489
476
|
>
|
|
490
|
-
|
|
477
|
+
{accounts.length > 1 && (
|
|
478
|
+
<Form.Dropdown
|
|
479
|
+
id='account'
|
|
480
|
+
title='From'
|
|
481
|
+
value={selectedAccount}
|
|
482
|
+
onChange={(v) => setSelectedAccount(Array.isArray(v) ? v[0] ?? initialAccount : v)}
|
|
483
|
+
>
|
|
484
|
+
{accounts.map((a) => (
|
|
485
|
+
<Form.Dropdown.Item
|
|
486
|
+
key={a.email}
|
|
487
|
+
value={a.email}
|
|
488
|
+
title={a.email}
|
|
489
|
+
/>
|
|
490
|
+
))}
|
|
491
|
+
</Form.Dropdown>
|
|
492
|
+
)}
|
|
493
|
+
{mode.type === 'forward' && (
|
|
494
|
+
<Form.TextField
|
|
495
|
+
id='to'
|
|
496
|
+
title='To'
|
|
497
|
+
placeholder='recipient@example.com'
|
|
498
|
+
/>
|
|
499
|
+
)}
|
|
491
500
|
<Form.TextArea
|
|
492
501
|
id='body'
|
|
493
502
|
title='Message'
|
|
494
|
-
placeholder=
|
|
503
|
+
placeholder={bodyPlaceholder}
|
|
495
504
|
/>
|
|
496
505
|
</Form>
|
|
497
506
|
)
|
|
@@ -504,10 +513,12 @@ function ForwardForm({
|
|
|
504
513
|
function ThreadDetail({
|
|
505
514
|
threadId,
|
|
506
515
|
account,
|
|
516
|
+
accounts,
|
|
507
517
|
revalidate,
|
|
508
518
|
}: {
|
|
509
519
|
threadId: string
|
|
510
520
|
account: string
|
|
521
|
+
accounts: AuthStatus[]
|
|
511
522
|
revalidate: () => void
|
|
512
523
|
}) {
|
|
513
524
|
const thread = useCachedPromise(
|
|
@@ -622,10 +633,11 @@ function ThreadDetail({
|
|
|
622
633
|
icon={Icon.Reply}
|
|
623
634
|
shortcut={{ modifiers: ['ctrl'], key: 'r' }}
|
|
624
635
|
target={
|
|
625
|
-
<
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
636
|
+
<ComposeForm
|
|
637
|
+
mode={{ type: 'reply', threadId }}
|
|
638
|
+
initialAccount={account}
|
|
639
|
+
accounts={accounts}
|
|
640
|
+
onSent={revalidate}
|
|
629
641
|
/>
|
|
630
642
|
}
|
|
631
643
|
/>
|
|
@@ -637,11 +649,11 @@ function ThreadDetail({
|
|
|
637
649
|
key: 'r',
|
|
638
650
|
}}
|
|
639
651
|
target={
|
|
640
|
-
<
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
652
|
+
<ComposeForm
|
|
653
|
+
mode={{ type: 'reply', threadId, replyAll: true }}
|
|
654
|
+
initialAccount={account}
|
|
655
|
+
accounts={accounts}
|
|
656
|
+
onSent={revalidate}
|
|
645
657
|
/>
|
|
646
658
|
}
|
|
647
659
|
/>
|
|
@@ -650,10 +662,11 @@ function ThreadDetail({
|
|
|
650
662
|
icon={Icon.Forward}
|
|
651
663
|
shortcut={{ modifiers: ['ctrl'], key: 'f' }}
|
|
652
664
|
target={
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
665
|
+
<ComposeForm
|
|
666
|
+
mode={{ type: 'forward', threadId }}
|
|
667
|
+
initialAccount={account}
|
|
668
|
+
accounts={accounts}
|
|
669
|
+
onSent={revalidate}
|
|
657
670
|
/>
|
|
658
671
|
}
|
|
659
672
|
/>
|
|
@@ -678,12 +691,35 @@ function ThreadDetail({
|
|
|
678
691
|
// Main Command
|
|
679
692
|
// ---------------------------------------------------------------------------
|
|
680
693
|
|
|
694
|
+
const CACHE_NAMESPACE = 'mail-tui'
|
|
695
|
+
|
|
681
696
|
export default function Command() {
|
|
682
|
-
const
|
|
697
|
+
const { push } = useNavigation()
|
|
698
|
+
const [selectedAccount, setSelectedAccount] = useCachedState(
|
|
699
|
+
'selectedAccount',
|
|
700
|
+
'all',
|
|
701
|
+
{ cacheNamespace: CACHE_NAMESPACE },
|
|
702
|
+
)
|
|
683
703
|
const [searchText, setSearchText] = useState('')
|
|
684
|
-
const [isShowingDetail, setIsShowingDetail] =
|
|
704
|
+
const [isShowingDetail, setIsShowingDetail] = useCachedState(
|
|
705
|
+
'isShowingDetail',
|
|
706
|
+
true,
|
|
707
|
+
{ cacheNamespace: CACHE_NAMESPACE },
|
|
708
|
+
)
|
|
685
709
|
const [selectedThreads, setSelectedThreads] = useState<string[]>([])
|
|
710
|
+
const [activeMutations, setActiveMutations] = useState(0)
|
|
711
|
+
const isMutating = activeMutations > 0
|
|
686
712
|
const { height: terminalRows } = useTerminalDimensions()
|
|
713
|
+
|
|
714
|
+
/** Wrap async mutation calls to track global loading state. */
|
|
715
|
+
const withMutation = useCallback(async <T,>(fn: () => Promise<T>): Promise<T> => {
|
|
716
|
+
setActiveMutations((n) => n + 1)
|
|
717
|
+
try {
|
|
718
|
+
return await fn()
|
|
719
|
+
} finally {
|
|
720
|
+
setActiveMutations((n) => n - 1)
|
|
721
|
+
}
|
|
722
|
+
}, [])
|
|
687
723
|
const pageSize = getPageSizeFromTerminalHeight(terminalRows)
|
|
688
724
|
|
|
689
725
|
const accounts = useAccounts()
|
|
@@ -859,41 +895,44 @@ export default function Command() {
|
|
|
859
895
|
) => {
|
|
860
896
|
if (selectedThreads.length === 0) return
|
|
861
897
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
898
|
+
await withMutation(async () => {
|
|
899
|
+
// Group selected threads by account
|
|
900
|
+
const byAccount = new Map<string, string[]>()
|
|
901
|
+
for (const tid of selectedThreads) {
|
|
902
|
+
const thread = allThreads.find((t: ThreadItem) => t.id === tid)
|
|
903
|
+
if (!thread) continue
|
|
904
|
+
const list = byAccount.get(thread.account) ?? []
|
|
905
|
+
list.push(tid)
|
|
906
|
+
byAccount.set(thread.account, list)
|
|
907
|
+
}
|
|
871
908
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
909
|
+
for (const [acct, ids] of byAccount) {
|
|
910
|
+
const { client } = await getClient([acct])
|
|
911
|
+
const result = await fn(client, ids)
|
|
912
|
+
if (result instanceof Error) {
|
|
913
|
+
await showFailureToast(result, {
|
|
914
|
+
title: `Failed to ${actionName}`,
|
|
915
|
+
})
|
|
916
|
+
return
|
|
917
|
+
}
|
|
880
918
|
}
|
|
881
|
-
}
|
|
882
919
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
920
|
+
await showToast({
|
|
921
|
+
style: Toast.Style.Success,
|
|
922
|
+
title: `${actionName}: ${selectedThreads.length} thread(s)`,
|
|
923
|
+
})
|
|
924
|
+
setSelectedThreads([])
|
|
925
|
+
revalidate()
|
|
886
926
|
})
|
|
887
|
-
setSelectedThreads([])
|
|
888
|
-
revalidate()
|
|
889
927
|
},
|
|
890
|
-
[selectedThreads, allThreads, revalidate],
|
|
928
|
+
[selectedThreads, allThreads, revalidate, withMutation],
|
|
891
929
|
)
|
|
892
930
|
|
|
893
931
|
return (
|
|
894
932
|
<List
|
|
895
|
-
isLoading={isLoading || accounts.isLoading}
|
|
933
|
+
isLoading={isLoading || accounts.isLoading || isMutating}
|
|
896
934
|
isShowingDetail={isShowingDetail}
|
|
935
|
+
spacingMode={LIST_SPACING_MODE}
|
|
897
936
|
searchBarPlaceholder='Search emails...'
|
|
898
937
|
onSearchTextChange={setSearchText}
|
|
899
938
|
throttle
|
|
@@ -903,10 +942,10 @@ export default function Command() {
|
|
|
903
942
|
<AccountDropdown
|
|
904
943
|
accounts={accountList}
|
|
905
944
|
value={selectedAccount}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
945
|
+
onChange={setSelectedAccount}
|
|
946
|
+
onAdded={handleAccountAdded}
|
|
947
|
+
onRemoved={handleAccountRemoved}
|
|
948
|
+
/>
|
|
910
949
|
) : undefined
|
|
911
950
|
}
|
|
912
951
|
>
|
|
@@ -1012,294 +1051,256 @@ export default function Command() {
|
|
|
1012
1051
|
detail={detail}
|
|
1013
1052
|
actions={
|
|
1014
1053
|
<ActionPanel>
|
|
1015
|
-
{
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1054
|
+
{hasSelection ? (
|
|
1055
|
+
// ─────────────────────────────────────────────
|
|
1056
|
+
// SELECTION MODE
|
|
1057
|
+
// ─────────────────────────────────────────────
|
|
1058
|
+
<>
|
|
1059
|
+
<ActionPanel.Section title='Selection'>
|
|
1060
|
+
<Action
|
|
1061
|
+
title={isSelected ? 'Deselect Thread' : 'Select Thread'}
|
|
1062
|
+
icon={isSelected ? Icon.Circle : Icon.CheckCircle}
|
|
1063
|
+
shortcut={{ modifiers: ['ctrl'], key: 'x' }}
|
|
1064
|
+
onAction={() => toggleSelection(thread.id)}
|
|
1065
|
+
/>
|
|
1066
|
+
<Action
|
|
1067
|
+
title={`Archive ${selectedThreads.length} Selected`}
|
|
1068
|
+
icon={Icon.Tray}
|
|
1069
|
+
onAction={() =>
|
|
1070
|
+
handleBulkAction('Archived', (c, ids) =>
|
|
1071
|
+
c.archive({ threadIds: ids }),
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
/>
|
|
1075
|
+
<Action
|
|
1076
|
+
title={`Mark ${selectedThreads.length} as Read`}
|
|
1077
|
+
icon={Icon.Eye}
|
|
1078
|
+
onAction={() =>
|
|
1079
|
+
handleBulkAction('Marked as read', (c, ids) =>
|
|
1080
|
+
c.markAsRead({ threadIds: ids }),
|
|
1081
|
+
)
|
|
1082
|
+
}
|
|
1083
|
+
/>
|
|
1084
|
+
<Action
|
|
1085
|
+
title={`Star ${selectedThreads.length} Selected`}
|
|
1086
|
+
icon={Icon.Star}
|
|
1087
|
+
onAction={() =>
|
|
1088
|
+
handleBulkAction('Starred', (c, ids) =>
|
|
1089
|
+
c.star({ threadIds: ids }),
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
/>
|
|
1093
|
+
<Action
|
|
1094
|
+
title={`Trash ${selectedThreads.length} Selected`}
|
|
1095
|
+
icon={Icon.Trash}
|
|
1096
|
+
onAction={() =>
|
|
1097
|
+
handleBulkAction('Trashed', async (c, ids) => {
|
|
1098
|
+
for (const id of ids) {
|
|
1099
|
+
await c.trash({ threadId: id })
|
|
1100
|
+
}
|
|
1101
|
+
})
|
|
1102
|
+
}
|
|
1103
|
+
/>
|
|
1104
|
+
<Action
|
|
1105
|
+
title='Deselect All'
|
|
1106
|
+
icon={Icon.XMarkCircle}
|
|
1107
|
+
onAction={() => setSelectedThreads([])}
|
|
1108
|
+
/>
|
|
1109
|
+
</ActionPanel.Section>
|
|
1110
|
+
<ActionPanel.Section>
|
|
1111
|
+
<Action
|
|
1112
|
+
title='Refresh'
|
|
1113
|
+
icon={Icon.ArrowClockwise}
|
|
1114
|
+
shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
|
|
1115
|
+
onAction={() => revalidate()}
|
|
1116
|
+
/>
|
|
1117
|
+
<Action
|
|
1118
|
+
title='Toggle Detail'
|
|
1119
|
+
icon={Icon.Sidebar}
|
|
1120
|
+
shortcut={{ modifiers: ['ctrl'], key: 'd' }}
|
|
1121
|
+
onAction={() => setIsShowingDetail((v) => !v)}
|
|
1122
|
+
/>
|
|
1123
|
+
</ActionPanel.Section>
|
|
1124
|
+
</>
|
|
1125
|
+
) : (
|
|
1126
|
+
// ─────────────────────────────────────────────
|
|
1127
|
+
// NORMAL MODE
|
|
1128
|
+
// ─────────────────────────────────────────────
|
|
1129
|
+
<>
|
|
1130
|
+
<ActionPanel.Section>
|
|
1131
|
+
<Action
|
|
1132
|
+
title='Open Thread'
|
|
1133
|
+
icon={Icon.Eye}
|
|
1134
|
+
onAction={async () => {
|
|
1135
|
+
push(
|
|
1136
|
+
<ThreadDetail
|
|
1137
|
+
threadId={thread.id}
|
|
1138
|
+
account={thread.account}
|
|
1139
|
+
accounts={accountList}
|
|
1140
|
+
revalidate={revalidate}
|
|
1141
|
+
/>,
|
|
1142
|
+
)
|
|
1143
|
+
if (thread.unread) {
|
|
1144
|
+
const { client } = await getClient([thread.account])
|
|
1145
|
+
const result = await client.markAsRead({ threadIds: [thread.id] })
|
|
1146
|
+
if (result instanceof Error) return
|
|
1147
|
+
revalidate()
|
|
1068
1148
|
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
/>
|
|
1072
|
-
<Action
|
|
1073
|
-
title='Deselect All'
|
|
1074
|
-
icon={Icon.XMarkCircle}
|
|
1075
|
-
onAction={() => setSelectedThreads([])}
|
|
1076
|
-
/>
|
|
1077
|
-
</ActionPanel.Section>
|
|
1078
|
-
)}
|
|
1079
|
-
|
|
1080
|
-
{/* Primary actions */}
|
|
1081
|
-
<ActionPanel.Section>
|
|
1082
|
-
<Action.Push
|
|
1083
|
-
title='Open Thread'
|
|
1084
|
-
icon={Icon.Eye}
|
|
1085
|
-
target={
|
|
1086
|
-
<ThreadDetail
|
|
1087
|
-
threadId={thread.id}
|
|
1088
|
-
account={thread.account}
|
|
1089
|
-
revalidate={revalidate}
|
|
1149
|
+
}}
|
|
1090
1150
|
/>
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
onAction={async () => {
|
|
1114
|
-
const { client } = await getClient([thread.account])
|
|
1115
|
-
const result = thread.unread
|
|
1116
|
-
? await client.markAsRead({
|
|
1117
|
-
threadIds: [thread.id],
|
|
1151
|
+
<Action
|
|
1152
|
+
title='Select Thread'
|
|
1153
|
+
icon={Icon.CheckCircle}
|
|
1154
|
+
shortcut={{ modifiers: ['ctrl'], key: 'x' }}
|
|
1155
|
+
onAction={() => toggleSelection(thread.id)}
|
|
1156
|
+
/>
|
|
1157
|
+
<Action
|
|
1158
|
+
title={thread.unread ? 'Mark as Read' : 'Mark as Unread'}
|
|
1159
|
+
icon={thread.unread ? Icon.Eye : Icon.EyeDisabled}
|
|
1160
|
+
shortcut={{ modifiers: ['ctrl'], key: 'u' }}
|
|
1161
|
+
onAction={() => withMutation(async () => {
|
|
1162
|
+
const { client } = await getClient([thread.account])
|
|
1163
|
+
const result = thread.unread
|
|
1164
|
+
? await client.markAsRead({ threadIds: [thread.id] })
|
|
1165
|
+
: await client.markAsUnread({ threadIds: [thread.id] })
|
|
1166
|
+
if (result instanceof Error) {
|
|
1167
|
+
await showFailureToast(result)
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
await showToast({
|
|
1171
|
+
style: Toast.Style.Success,
|
|
1172
|
+
title: thread.unread ? 'Marked as read' : 'Marked as unread',
|
|
1118
1173
|
})
|
|
1119
|
-
|
|
1120
|
-
|
|
1174
|
+
revalidate()
|
|
1175
|
+
})}
|
|
1176
|
+
/>
|
|
1177
|
+
<Action
|
|
1178
|
+
title='Archive'
|
|
1179
|
+
icon={Icon.Tray}
|
|
1180
|
+
shortcut={{ modifiers: ['ctrl'], key: 'e' }}
|
|
1181
|
+
onAction={() => withMutation(async () => {
|
|
1182
|
+
const { client } = await getClient([thread.account])
|
|
1183
|
+
const result = await client.archive({ threadIds: [thread.id] })
|
|
1184
|
+
if (result instanceof Error) {
|
|
1185
|
+
await showFailureToast(result)
|
|
1186
|
+
return
|
|
1187
|
+
}
|
|
1188
|
+
await showToast({
|
|
1189
|
+
style: Toast.Style.Success,
|
|
1190
|
+
title: 'Archived',
|
|
1121
1191
|
})
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
onAction={async () => {
|
|
1143
|
-
const { client } = await getClient([thread.account])
|
|
1144
|
-
const result = await client.archive({
|
|
1145
|
-
threadIds: [thread.id],
|
|
1146
|
-
})
|
|
1147
|
-
if (result instanceof Error) {
|
|
1148
|
-
await showFailureToast(result)
|
|
1149
|
-
return
|
|
1150
|
-
}
|
|
1151
|
-
await showToast({
|
|
1152
|
-
style: Toast.Style.Success,
|
|
1153
|
-
title: 'Archived',
|
|
1154
|
-
})
|
|
1155
|
-
revalidate()
|
|
1156
|
-
}}
|
|
1157
|
-
/>
|
|
1158
|
-
<Action
|
|
1159
|
-
title={
|
|
1160
|
-
thread.labelIds.includes('STARRED')
|
|
1161
|
-
? 'Unstar'
|
|
1162
|
-
: 'Star'
|
|
1163
|
-
}
|
|
1164
|
-
icon={Icon.Star}
|
|
1165
|
-
shortcut={{
|
|
1166
|
-
modifiers: ['ctrl'],
|
|
1167
|
-
key: 's',
|
|
1168
|
-
}}
|
|
1169
|
-
onAction={async () => {
|
|
1170
|
-
const { client } = await getClient([thread.account])
|
|
1171
|
-
const isStarred = thread.labelIds.includes('STARRED')
|
|
1172
|
-
const result = isStarred
|
|
1173
|
-
? await client.unstar({
|
|
1174
|
-
threadIds: [thread.id],
|
|
1192
|
+
revalidate()
|
|
1193
|
+
})}
|
|
1194
|
+
/>
|
|
1195
|
+
<Action
|
|
1196
|
+
title={thread.labelIds.includes('STARRED') ? 'Unstar' : 'Star'}
|
|
1197
|
+
icon={Icon.Star}
|
|
1198
|
+
shortcut={{ modifiers: ['ctrl'], key: 's' }}
|
|
1199
|
+
onAction={() => withMutation(async () => {
|
|
1200
|
+
const { client } = await getClient([thread.account])
|
|
1201
|
+
const isStarred = thread.labelIds.includes('STARRED')
|
|
1202
|
+
const result = isStarred
|
|
1203
|
+
? await client.unstar({ threadIds: [thread.id] })
|
|
1204
|
+
: await client.star({ threadIds: [thread.id] })
|
|
1205
|
+
if (result instanceof Error) {
|
|
1206
|
+
await showFailureToast(result)
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
await showToast({
|
|
1210
|
+
style: Toast.Style.Success,
|
|
1211
|
+
title: isStarred ? 'Unstarred' : 'Starred',
|
|
1175
1212
|
})
|
|
1176
|
-
|
|
1177
|
-
|
|
1213
|
+
revalidate()
|
|
1214
|
+
})}
|
|
1215
|
+
/>
|
|
1216
|
+
</ActionPanel.Section>
|
|
1217
|
+
<ActionPanel.Section title='Reply & Forward'>
|
|
1218
|
+
<Action.Push
|
|
1219
|
+
title='Reply'
|
|
1220
|
+
icon={Icon.Reply}
|
|
1221
|
+
shortcut={{ modifiers: ['ctrl'], key: 'r' }}
|
|
1222
|
+
target={
|
|
1223
|
+
<ComposeForm
|
|
1224
|
+
mode={{ type: 'reply', threadId: thread.id }}
|
|
1225
|
+
initialAccount={thread.account}
|
|
1226
|
+
accounts={accountList}
|
|
1227
|
+
onSent={revalidate}
|
|
1228
|
+
/>
|
|
1229
|
+
}
|
|
1230
|
+
/>
|
|
1231
|
+
<Action.Push
|
|
1232
|
+
title='Reply All'
|
|
1233
|
+
icon={Icon.Reply}
|
|
1234
|
+
shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
|
|
1235
|
+
target={
|
|
1236
|
+
<ComposeForm
|
|
1237
|
+
mode={{ type: 'reply', threadId: thread.id, replyAll: true }}
|
|
1238
|
+
initialAccount={thread.account}
|
|
1239
|
+
accounts={accountList}
|
|
1240
|
+
onSent={revalidate}
|
|
1241
|
+
/>
|
|
1242
|
+
}
|
|
1243
|
+
/>
|
|
1244
|
+
<Action.Push
|
|
1245
|
+
title='Forward'
|
|
1246
|
+
icon={Icon.Forward}
|
|
1247
|
+
shortcut={{ modifiers: ['ctrl'], key: 'f' }}
|
|
1248
|
+
target={
|
|
1249
|
+
<ComposeForm
|
|
1250
|
+
mode={{ type: 'forward', threadId: thread.id }}
|
|
1251
|
+
initialAccount={thread.account}
|
|
1252
|
+
accounts={accountList}
|
|
1253
|
+
onSent={revalidate}
|
|
1254
|
+
/>
|
|
1255
|
+
}
|
|
1256
|
+
/>
|
|
1257
|
+
</ActionPanel.Section>
|
|
1258
|
+
<ActionPanel.Section title='Copy'>
|
|
1259
|
+
<Action.CopyToClipboard
|
|
1260
|
+
title='Copy Thread ID'
|
|
1261
|
+
content={thread.id}
|
|
1262
|
+
/>
|
|
1263
|
+
<Action.CopyToClipboard
|
|
1264
|
+
title='Copy Subject'
|
|
1265
|
+
content={thread.subject}
|
|
1266
|
+
/>
|
|
1267
|
+
<Action.CopyToClipboard
|
|
1268
|
+
title='Copy Sender Email'
|
|
1269
|
+
content={thread.from.email}
|
|
1270
|
+
/>
|
|
1271
|
+
</ActionPanel.Section>
|
|
1272
|
+
<ActionPanel.Section>
|
|
1273
|
+
<Action
|
|
1274
|
+
title='Trash'
|
|
1275
|
+
icon={Icon.Trash}
|
|
1276
|
+
shortcut={{ modifiers: ['ctrl'], key: 'backspace' }}
|
|
1277
|
+
onAction={() => withMutation(async () => {
|
|
1278
|
+
const { client } = await getClient([thread.account])
|
|
1279
|
+
await client.trash({ threadId: thread.id })
|
|
1280
|
+
await showToast({
|
|
1281
|
+
style: Toast.Style.Success,
|
|
1282
|
+
title: 'Trashed',
|
|
1178
1283
|
})
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
return
|
|
1182
|
-
}
|
|
1183
|
-
await showToast({
|
|
1184
|
-
style: Toast.Style.Success,
|
|
1185
|
-
title: isStarred ? 'Unstarred' : 'Starred',
|
|
1186
|
-
})
|
|
1187
|
-
revalidate()
|
|
1188
|
-
}}
|
|
1189
|
-
/>
|
|
1190
|
-
</ActionPanel.Section>
|
|
1191
|
-
|
|
1192
|
-
{/* Reply & Forward */}
|
|
1193
|
-
<ActionPanel.Section title='Reply & Forward'>
|
|
1194
|
-
<Action.Push
|
|
1195
|
-
title='Reply'
|
|
1196
|
-
icon={Icon.Reply}
|
|
1197
|
-
shortcut={{
|
|
1198
|
-
modifiers: ['ctrl'],
|
|
1199
|
-
key: 'r',
|
|
1200
|
-
}}
|
|
1201
|
-
target={
|
|
1202
|
-
<ReplyForm
|
|
1203
|
-
threadId={thread.id}
|
|
1204
|
-
account={thread.account}
|
|
1205
|
-
revalidate={revalidate}
|
|
1284
|
+
revalidate()
|
|
1285
|
+
})}
|
|
1206
1286
|
/>
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
key: 'r',
|
|
1215
|
-
}}
|
|
1216
|
-
target={
|
|
1217
|
-
<ReplyForm
|
|
1218
|
-
threadId={thread.id}
|
|
1219
|
-
account={thread.account}
|
|
1220
|
-
replyAll
|
|
1221
|
-
revalidate={revalidate}
|
|
1287
|
+
</ActionPanel.Section>
|
|
1288
|
+
<ActionPanel.Section>
|
|
1289
|
+
<Action
|
|
1290
|
+
title='Refresh'
|
|
1291
|
+
icon={Icon.ArrowClockwise}
|
|
1292
|
+
shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }}
|
|
1293
|
+
onAction={() => revalidate()}
|
|
1222
1294
|
/>
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
shortcut={{
|
|
1229
|
-
modifiers: ['ctrl'],
|
|
1230
|
-
key: 'f',
|
|
1231
|
-
}}
|
|
1232
|
-
target={
|
|
1233
|
-
<ForwardForm
|
|
1234
|
-
threadId={thread.id}
|
|
1235
|
-
account={thread.account}
|
|
1236
|
-
revalidate={revalidate}
|
|
1295
|
+
<Action
|
|
1296
|
+
title='Toggle Detail'
|
|
1297
|
+
icon={Icon.Sidebar}
|
|
1298
|
+
shortcut={{ modifiers: ['ctrl'], key: 'd' }}
|
|
1299
|
+
onAction={() => setIsShowingDetail((v) => !v)}
|
|
1237
1300
|
/>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
{/* Copy */}
|
|
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
|
-
/>
|
|
1256
|
-
</ActionPanel.Section>
|
|
1257
|
-
|
|
1258
|
-
{/* Danger */}
|
|
1259
|
-
<ActionPanel.Section>
|
|
1260
|
-
<Action
|
|
1261
|
-
title='Trash'
|
|
1262
|
-
icon={Icon.Trash}
|
|
1263
|
-
style={Action.Style.Destructive}
|
|
1264
|
-
shortcut={{
|
|
1265
|
-
modifiers: ['ctrl'],
|
|
1266
|
-
key: 'backspace',
|
|
1267
|
-
}}
|
|
1268
|
-
onAction={async () => {
|
|
1269
|
-
const { client } = await getClient([thread.account])
|
|
1270
|
-
await client.trash({
|
|
1271
|
-
threadId: thread.id,
|
|
1272
|
-
})
|
|
1273
|
-
await showToast({
|
|
1274
|
-
style: Toast.Style.Success,
|
|
1275
|
-
title: 'Trashed',
|
|
1276
|
-
})
|
|
1277
|
-
revalidate()
|
|
1278
|
-
}}
|
|
1279
|
-
/>
|
|
1280
|
-
</ActionPanel.Section>
|
|
1281
|
-
|
|
1282
|
-
{/* Utility */}
|
|
1283
|
-
<ActionPanel.Section>
|
|
1284
|
-
<Action
|
|
1285
|
-
title='Refresh'
|
|
1286
|
-
icon={Icon.ArrowClockwise}
|
|
1287
|
-
shortcut={{
|
|
1288
|
-
modifiers: ['ctrl', 'shift'],
|
|
1289
|
-
key: 'r',
|
|
1290
|
-
}}
|
|
1291
|
-
onAction={() => revalidate()}
|
|
1292
|
-
/>
|
|
1293
|
-
<Action
|
|
1294
|
-
title='Toggle Detail'
|
|
1295
|
-
icon={Icon.Sidebar}
|
|
1296
|
-
shortcut={{
|
|
1297
|
-
modifiers: ['ctrl'],
|
|
1298
|
-
key: 'd',
|
|
1299
|
-
}}
|
|
1300
|
-
onAction={() => setIsShowingDetail((v) => !v)}
|
|
1301
|
-
/>
|
|
1302
|
-
</ActionPanel.Section>
|
|
1301
|
+
</ActionPanel.Section>
|
|
1302
|
+
</>
|
|
1303
|
+
)}
|
|
1303
1304
|
</ActionPanel>
|
|
1304
1305
|
}
|
|
1305
1306
|
/>
|