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/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
- return Math.max(MIN_PAGE_SIZE, rows - VISIBLE_ROWS_OFFSET)
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
- // Reply Form
376
+ // Compose Form (unified: reply, reply all, forward)
372
377
  // ---------------------------------------------------------------------------
373
378
 
374
- function ReplyForm({
375
- threadId,
376
- account,
377
- replyAll,
378
- revalidate,
379
- }: {
380
- threadId: string
381
- account: string
382
- replyAll?: boolean
383
- revalidate: () => void
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
- const handleSubmit = async (values: { body: string }) => {
389
- if (!values.body?.trim()) {
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: 'Body is required',
412
+ title: 'Recipient is required',
393
413
  })
394
414
  return
395
415
  }
396
- setIsLoading(true)
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: 'Recipient is required',
419
+ title: 'Message is required',
453
420
  })
454
421
  return
455
422
  }
423
+
456
424
  setIsLoading(true)
457
- const recipients = values.to
458
- .split(',')
459
- .map((e) => ({ email: e.trim() }))
460
- .filter((e) => e.email)
461
- const { client } = await getClient([account])
462
- const result = await client.forwardThread({
463
- threadId,
464
- to: recipients,
465
- body: values.body || undefined,
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, { title: 'Failed to forward' })
449
+ await showFailureToast(result, {
450
+ title: mode.type === 'forward' ? 'Failed to forward' : 'Failed to send reply',
451
+ })
470
452
  return
471
453
  }
472
- await showToast({
473
- style: Toast.Style.Success,
474
- title: `Forwarded to ${values.to}`,
475
- })
476
- revalidate()
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='Forward'
467
+ navigationTitle={navigationTitle}
484
468
  actions={
485
469
  <ActionPanel>
486
- <Action.SubmitForm title='Forward' onSubmit={handleSubmit} />
470
+ <Action.SubmitForm
471
+ title={mode.type === 'forward' ? 'Forward' : 'Send Reply'}
472
+ onSubmit={handleSubmit}
473
+ />
487
474
  </ActionPanel>
488
475
  }
489
476
  >
490
- <Form.TextField id='to' title='To' placeholder='recipient@example.com' />
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='Optional message to prepend...'
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
- <ReplyForm
626
- threadId={threadId}
627
- account={account}
628
- revalidate={revalidate}
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
- <ReplyForm
641
- threadId={threadId}
642
- account={account}
643
- replyAll
644
- revalidate={revalidate}
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
- <ForwardForm
654
- threadId={threadId}
655
- account={account}
656
- revalidate={revalidate}
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 [selectedAccount, setSelectedAccount] = useState('all')
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] = useState(true)
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
- // Group selected threads by account
863
- const byAccount = new Map<string, string[]>()
864
- for (const tid of selectedThreads) {
865
- const thread = allThreads.find((t: ThreadItem) => t.id === tid)
866
- if (!thread) continue
867
- const list = byAccount.get(thread.account) ?? []
868
- list.push(tid)
869
- byAccount.set(thread.account, list)
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
- for (const [acct, ids] of byAccount) {
873
- const { client } = await getClient([acct])
874
- const result = await fn(client, ids)
875
- if (result instanceof Error) {
876
- await showFailureToast(result, {
877
- title: `Failed to ${actionName}`,
878
- })
879
- return
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
- await showToast({
884
- style: Toast.Style.Success,
885
- title: `${actionName}: ${selectedThreads.length} thread(s)`,
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
- onChange={setSelectedAccount}
907
- onAdded={handleAccountAdded}
908
- onRemoved={handleAccountRemoved}
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
- {/* Selection actions (when items are selected) */}
1016
- {hasSelection && (
1017
- <ActionPanel.Section title='Selection'>
1018
- <Action
1019
- title={
1020
- isSelected ? 'Deselect Thread' : 'Select Thread'
1021
- }
1022
- icon={isSelected ? Icon.CheckCircle : Icon.Circle}
1023
- onAction={() => toggleSelection(thread.id)}
1024
- />
1025
- <Action
1026
- title={`Archive ${selectedThreads.length} Selected`}
1027
- icon={Icon.Tray}
1028
- onAction={() =>
1029
- handleBulkAction('Archived', (c, ids) =>
1030
- c.archive({
1031
- threadIds: ids,
1032
- }),
1033
- )
1034
- }
1035
- />
1036
- <Action
1037
- title={`Mark ${selectedThreads.length} as Read`}
1038
- icon={Icon.Eye}
1039
- onAction={() =>
1040
- handleBulkAction('Marked as read', (c, ids) =>
1041
- c.markAsRead({
1042
- threadIds: ids,
1043
- }),
1044
- )
1045
- }
1046
- />
1047
- <Action
1048
- title={`Star ${selectedThreads.length} Selected`}
1049
- icon={Icon.Star}
1050
- onAction={() =>
1051
- handleBulkAction('Starred', (c, ids) =>
1052
- c.star({
1053
- threadIds: ids,
1054
- }),
1055
- )
1056
- }
1057
- />
1058
- <Action
1059
- title={`Trash ${selectedThreads.length} Selected`}
1060
- icon={Icon.Trash}
1061
- style={Action.Style.Destructive}
1062
- onAction={() =>
1063
- handleBulkAction('Trashed', async (c, ids) => {
1064
- for (const id of ids) {
1065
- await c.trash({
1066
- threadId: id,
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
- {!hasSelection && (
1094
- <Action
1095
- title='Select Thread'
1096
- icon={Icon.CheckCircle}
1097
- shortcut={{
1098
- modifiers: ['ctrl'],
1099
- key: 'x',
1100
- }}
1101
- onAction={() => toggleSelection(thread.id)}
1102
- />
1103
- )}
1104
- <Action
1105
- title={
1106
- thread.unread ? 'Mark as Read' : 'Mark as Unread'
1107
- }
1108
- icon={thread.unread ? Icon.Eye : Icon.EyeDisabled}
1109
- shortcut={{
1110
- modifiers: ['ctrl'],
1111
- key: 'u',
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
- : await client.markAsUnread({
1120
- threadIds: [thread.id],
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
- if (result instanceof Error) {
1123
- await showFailureToast(result)
1124
- return
1125
- }
1126
- await showToast({
1127
- style: Toast.Style.Success,
1128
- title: thread.unread
1129
- ? 'Marked as read'
1130
- : 'Marked as unread',
1131
- })
1132
- revalidate()
1133
- }}
1134
- />
1135
- <Action
1136
- title='Archive'
1137
- icon={Icon.Tray}
1138
- shortcut={{
1139
- modifiers: ['ctrl'],
1140
- key: 'e',
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
- : await client.star({
1177
- threadIds: [thread.id],
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
- if (result instanceof Error) {
1180
- await showFailureToast(result)
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
- <Action.Push
1210
- title='Reply All'
1211
- icon={Icon.Reply}
1212
- shortcut={{
1213
- modifiers: ['ctrl', 'shift'],
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
- <Action.Push
1226
- title='Forward'
1227
- icon={Icon.Forward}
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
- </ActionPanel.Section>
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
  />