@rapidmx/web-client 0.3.1 → 0.5.0

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.
Files changed (202) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/branding/index.tsx +39 -404
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/domains/[uid].tsx +92 -259
  6. package/apps/admin/encryption-policy/index.tsx +19 -0
  7. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  8. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  9. package/apps/admin/index.tsx +118 -82
  10. package/apps/admin/ingest-queue/index.tsx +44 -5
  11. package/apps/admin/mailbox-policy/index.tsx +19 -0
  12. package/apps/admin/mailboxes/[uid].tsx +211 -184
  13. package/apps/admin/mailboxes/new/index.tsx +28 -291
  14. package/apps/admin/plugins/index.tsx +15 -0
  15. package/apps/admin/quarantine/index.tsx +46 -14
  16. package/apps/admin/retention-policy/index.tsx +39 -132
  17. package/apps/admin/setup/index.tsx +15 -0
  18. package/apps/admin/transport-rules/[uid].tsx +30 -1
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  20. package/apps/admin/transport-rules/index.tsx +24 -6
  21. package/apps/admin/transport-rules/new/index.tsx +30 -1
  22. package/apps/escrow/_layout.tsx +2 -3
  23. package/apps/escrow/audit-log/index.tsx +196 -168
  24. package/apps/escrow/matters/[uid].tsx +621 -521
  25. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  26. package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
  27. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  28. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  29. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  30. package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
  31. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
  32. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
  33. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  34. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
  35. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
  36. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
  37. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
  38. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
  39. package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
  40. package/apps/shared/components/admin/signOut.ts +79 -0
  41. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  42. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  43. package/apps/shared/components/calendar/EventModal.tsx +717 -513
  44. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  45. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  46. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  47. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  48. package/apps/shared/components/calendar/allDay.ts +124 -0
  49. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  50. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  51. package/apps/shared/components/contacts/ContactForm.tsx +377 -314
  52. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  53. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  54. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  55. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  56. package/apps/shared/components/layout/AppShell.tsx +270 -169
  57. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  58. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
  59. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  60. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  61. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
  62. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
  64. package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
  65. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
  66. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  67. package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
  68. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  69. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  70. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  71. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  72. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  73. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  74. package/apps/shared/mail/listAllPages.ts +39 -0
  75. package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
  76. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  77. package/apps/shared/search/localIndexBuilder.ts +481 -0
  78. package/apps/shared/search/localIndexKey.ts +27 -0
  79. package/apps/shared/search/localIndexRpcClient.ts +316 -0
  80. package/apps/shared/search/localIndexSchema.ts +236 -0
  81. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  82. package/apps/shared/search/localIndexStorage.ts +98 -0
  83. package/apps/shared/search/localIndexVFS.ts +266 -0
  84. package/apps/shared/search/localIndexWorker.ts +926 -0
  85. package/apps/shared/search/searchTier2.ts +79 -0
  86. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  87. package/apps/www/calendar/index.tsx +438 -407
  88. package/apps/www/contacts/[uid].tsx +21 -2
  89. package/apps/www/contacts/index.tsx +148 -17
  90. package/apps/www/index.tsx +1314 -486
  91. package/apps/www/messages/[uid].tsx +23 -7
  92. package/apps/www/settings/auto-reply/index.tsx +134 -129
  93. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  94. package/apps/www/settings/encryption/index.tsx +800 -215
  95. package/apps/www/settings/filters/[uid].tsx +171 -155
  96. package/apps/www/settings/filters/new/index.tsx +8 -1
  97. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  98. package/apps/www/settings/privacy/index.tsx +494 -436
  99. package/apps/www/settings/read-receipts/index.tsx +148 -125
  100. package/apps/www/settings/sharing/index.tsx +274 -0
  101. package/apps/www/tasks/index.tsx +638 -530
  102. package/dist/apps/admin/audit-log/index.js +38 -8
  103. package/dist/apps/admin/branding/index.js +4 -98
  104. package/dist/apps/admin/data-requests/index.js +52 -72
  105. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  106. package/dist/apps/admin/domains/[uid].js +5 -68
  107. package/dist/apps/admin/encryption-policy/index.js +8 -0
  108. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.js +17 -1
  111. package/dist/apps/admin/ingest-queue/index.js +24 -6
  112. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  113. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  114. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  115. package/dist/apps/admin/plugins/index.js +6 -0
  116. package/dist/apps/admin/quarantine/index.js +20 -11
  117. package/dist/apps/admin/retention-policy/index.js +3 -36
  118. package/dist/apps/admin/setup/index.js +6 -0
  119. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  120. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  121. package/dist/apps/admin/transport-rules/index.js +22 -6
  122. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  123. package/dist/apps/escrow/_layout.js +3 -3
  124. package/dist/apps/escrow/audit-log/index.js +26 -1
  125. package/dist/apps/escrow/matters/[uid].js +77 -42
  126. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  127. package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
  128. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  129. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  130. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  131. package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
  132. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
  133. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
  134. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  135. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
  136. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
  137. package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
  138. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
  139. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
  140. package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
  141. package/dist/apps/shared/components/admin/signOut.js +75 -0
  142. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  143. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  144. package/dist/apps/shared/components/calendar/EventModal.js +154 -21
  145. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  146. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  147. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  148. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  149. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  150. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  151. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  152. package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
  153. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  154. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  155. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  156. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  157. package/dist/apps/shared/components/layout/AppShell.js +100 -8
  158. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  159. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
  160. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  161. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  162. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
  163. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  164. package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
  165. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  166. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
  167. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  168. package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
  169. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  170. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  171. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  172. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  173. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  174. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  175. package/dist/apps/shared/mail/listAllPages.js +25 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  178. package/dist/apps/shared/search/localIndexBuilder.js +408 -0
  179. package/dist/apps/shared/search/localIndexKey.js +25 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
  181. package/dist/apps/shared/search/localIndexSchema.js +185 -0
  182. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  183. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  184. package/dist/apps/shared/search/localIndexVFS.js +258 -0
  185. package/dist/apps/shared/search/localIndexWorker.js +687 -0
  186. package/dist/apps/shared/search/searchTier2.js +38 -0
  187. package/dist/apps/www/calendar/index.js +38 -18
  188. package/dist/apps/www/contacts/[uid].js +13 -2
  189. package/dist/apps/www/contacts/index.js +94 -15
  190. package/dist/apps/www/index.js +770 -137
  191. package/dist/apps/www/messages/[uid].js +23 -7
  192. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  193. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  194. package/dist/apps/www/settings/encryption/index.js +541 -96
  195. package/dist/apps/www/settings/filters/[uid].js +22 -7
  196. package/dist/apps/www/settings/filters/new/index.js +8 -2
  197. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  198. package/dist/apps/www/settings/privacy/index.js +57 -22
  199. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  200. package/dist/apps/www/settings/sharing/index.js +124 -0
  201. package/dist/apps/www/tasks/index.js +76 -14
  202. package/package.json +3 -2
@@ -1,148 +1,150 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useEffect, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { MessageClassification } from "@rapidmx/react-shared/mail/mailApi.js";
8
- import {
9
- FocusedInboxOverride,
10
- createFocusedInboxOverride,
11
- deleteFocusedInboxOverride,
12
- listFocusedInboxOverrides,
13
- } from "@rapidmx/react-shared/mail/focusedInboxOverridesApi.js";
14
- import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
15
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
16
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
17
-
18
- export type SettingsFocusedInboxPageProps = Omit<SettingsShellProps, "active">;
19
-
20
- export default function SettingsFocusedInboxPage(props: SettingsFocusedInboxPageProps) {
21
- return (
22
- <SettingsShell {...props} active="focused-inbox">
23
- <FocusedInboxContent />
24
- </SettingsShell>
25
- );
26
- }
27
-
28
- function FocusedInboxContent() {
29
- const { mailboxUid } = useSettingsShell();
30
- const [overrides, setOverrides] = useState<FocusedInboxOverride[]>([]);
31
- const [loading, setLoading] = useState(true);
32
- const [error, setError] = useState<string | null>(null);
33
- const [saving, setSaving] = useState(false);
34
- const [newSender, setNewSender] = useState("");
35
- const [newClassifyAs, setNewClassifyAs] = useState<MessageClassification>("other");
36
-
37
- function reload() {
38
- setLoading(true);
39
- setError(null);
40
- // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
41
- // established non-null pattern as `apps/www/settings/filters/index.tsx`.
42
- listFocusedInboxOverrides(mailboxUid!)
43
- .then(setOverrides)
44
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load your Focused Inbox rules."))
45
- .finally(() => setLoading(false));
46
- }
47
-
48
- useEffect(reload, [mailboxUid]);
49
-
50
- async function handleAdd(e: FormEvent) {
51
- e.preventDefault();
52
- const senderAddress = newSender.trim();
53
- if (!senderAddress) {
54
- return;
55
- }
56
- setSaving(true);
57
- setError(null);
58
- try {
59
- await createFocusedInboxOverride({ mailboxUid: mailboxUid!, senderAddress, classifyAs: newClassifyAs });
60
- setNewSender("");
61
- reload();
62
- } catch (err) {
63
- setError(err instanceof ApiRequestError ? err.message : "Could not add this rule.");
64
- } finally {
65
- setSaving(false);
66
- }
67
- }
68
-
69
- async function handleRemove(override: FocusedInboxOverride) {
70
- setSaving(true);
71
- setError(null);
72
- try {
73
- await deleteFocusedInboxOverride(override.uid, override.version);
74
- reload();
75
- } catch (err) {
76
- setError(err instanceof ApiRequestError ? err.message : "Could not remove this rule.");
77
- } finally {
78
- setSaving(false);
79
- }
80
- }
81
-
82
- return (
83
- <div className="flex-1 min-w-0 overflow-y-auto p-6">
84
- <div className="max-w-xl">
85
- <h1 className="text-lg font-bold tracking-tight mb-1">Focused Inbox</h1>
86
- <p className="text-sm text-text-muted mb-4">
87
- Mail from a sender listed here always goes to the matching half of your Inbox, overriding
88
- the automatic Focused/Other classification. Add one directly, or use &ldquo;Always for this
89
- sender&rdquo; when moving a message.
90
- </p>
91
-
92
- {error && <Alert>{error}</Alert>}
93
-
94
- {loading ? (
95
- <p className="text-sm text-text-muted">Loading&hellip;</p>
96
- ) : (
97
- <ul className="flex flex-col gap-2 mb-4">
98
- {overrides.length === 0 && <li className="text-sm text-text-muted">No rules yet.</li>}
99
- {overrides.map((override) => (
100
- <li
101
- key={override.uid}
102
- className="flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm"
103
- >
104
- <div>
105
- <div className="text-sm font-medium">{override.senderAddress}</div>
106
- <div className="text-xs text-text-muted">
107
- Always {override.classifyAs === "other" ? "Other" : "Focused"}
108
- </div>
109
- </div>
110
- <button
111
- type="button"
112
- onClick={() => handleRemove(override)}
113
- disabled={saving}
114
- className="text-sm text-danger hover:underline disabled:opacity-55"
115
- >
116
- Remove
117
- </button>
118
- </li>
119
- ))}
120
- </ul>
121
- )}
122
-
123
- <form onSubmit={handleAdd} className="flex gap-2">
124
- <input
125
- type="text"
126
- aria-label="Sender address"
127
- value={newSender}
128
- onChange={(e) => setNewSender(e.target.value)}
129
- placeholder="sender@example.com"
130
- className="flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
131
- />
132
- <select
133
- aria-label="Classify as"
134
- value={newClassifyAs}
135
- onChange={(e) => setNewClassifyAs(e.target.value as MessageClassification)}
136
- className="text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
137
- >
138
- <option value="other">Other</option>
139
- <option value="focused">Focused</option>
140
- </select>
141
- <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
142
- Add
143
- </Button>
144
- </form>
145
- </div>
146
- </div>
147
- );
148
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useEffect, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { MessageClassification } from "@rapidmx/react-shared/mail/mailApi.js";
8
+ import {
9
+ FocusedInboxOverride,
10
+ createFocusedInboxOverride,
11
+ deleteFocusedInboxOverride,
12
+ listFocusedInboxOverrides,
13
+ } from "@rapidmx/react-shared/mail/focusedInboxOverridesApi.js";
14
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
15
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
16
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
17
+
18
+ export type SettingsFocusedInboxPageProps = Omit<SettingsShellProps, "active">;
19
+
20
+ export default function SettingsFocusedInboxPage(props: SettingsFocusedInboxPageProps) {
21
+ return (
22
+ <SettingsShell {...props} active="focused-inbox">
23
+ <FocusedInboxContent />
24
+ </SettingsShell>
25
+ );
26
+ }
27
+
28
+ function FocusedInboxContent() {
29
+ const { mailboxUid } = useSettingsShell();
30
+ const [overrides, setOverrides] = useState<FocusedInboxOverride[]>([]);
31
+ const [loading, setLoading] = useState(true);
32
+ const [error, setError] = useState<string | null>(null);
33
+ const [saving, setSaving] = useState(false);
34
+ const [newSender, setNewSender] = useState("");
35
+ const [newClassifyAs, setNewClassifyAs] = useState<MessageClassification>("other");
36
+
37
+ function reload() {
38
+ setLoading(true);
39
+ setError(null);
40
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
41
+ // established non-null pattern as `apps/www/settings/filters/index.tsx`.
42
+ listFocusedInboxOverrides(mailboxUid!)
43
+ .then(setOverrides)
44
+ .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load your Focused Inbox rules."))
45
+ .finally(() => setLoading(false));
46
+ }
47
+
48
+ useEffect(reload, [mailboxUid]);
49
+
50
+ async function handleAdd(e: FormEvent) {
51
+ e.preventDefault();
52
+ // Lowercased to match restapi's own `normalizeAddress()` - what gets sent is exactly what gets
53
+ // stored and matched against, rather than relying on the server to silently rewrite it.
54
+ const senderAddress = newSender.trim().toLowerCase();
55
+ if (!senderAddress) {
56
+ return;
57
+ }
58
+ setSaving(true);
59
+ setError(null);
60
+ try {
61
+ await createFocusedInboxOverride({ mailboxUid: mailboxUid!, senderAddress, classifyAs: newClassifyAs });
62
+ setNewSender("");
63
+ reload();
64
+ } catch (err) {
65
+ setError(err instanceof ApiRequestError ? err.message : "Could not add this rule.");
66
+ } finally {
67
+ setSaving(false);
68
+ }
69
+ }
70
+
71
+ async function handleRemove(override: FocusedInboxOverride) {
72
+ setSaving(true);
73
+ setError(null);
74
+ try {
75
+ await deleteFocusedInboxOverride(override.uid, override.version);
76
+ reload();
77
+ } catch (err) {
78
+ setError(err instanceof ApiRequestError ? err.message : "Could not remove this rule.");
79
+ } finally {
80
+ setSaving(false);
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
86
+ <div className="max-w-xl">
87
+ <h1 className="text-lg font-bold tracking-tight mb-1">Focused Inbox</h1>
88
+ <p className="text-sm text-text-muted mb-4">
89
+ Mail from a sender listed here always goes to the matching half of your Inbox, overriding
90
+ the automatic Focused/Other classification. Add one directly, or use &ldquo;Always for this
91
+ sender&rdquo; when moving a message.
92
+ </p>
93
+
94
+ {error && <Alert>{error}</Alert>}
95
+
96
+ {loading ? (
97
+ <p className="text-sm text-text-muted">Loading&hellip;</p>
98
+ ) : (
99
+ <ul className="flex flex-col gap-2 mb-4">
100
+ {overrides.length === 0 && <li className="text-sm text-text-muted">No rules yet.</li>}
101
+ {overrides.map((override) => (
102
+ <li
103
+ key={override.uid}
104
+ className="flex items-center justify-between gap-3 py-2 px-3 border border-border rounded-sm"
105
+ >
106
+ <div>
107
+ <div className="text-sm font-medium">{override.senderAddress}</div>
108
+ <div className="text-xs text-text-muted">
109
+ Always {override.classifyAs === "other" ? "Other" : "Focused"}
110
+ </div>
111
+ </div>
112
+ <button
113
+ type="button"
114
+ onClick={() => handleRemove(override)}
115
+ disabled={saving}
116
+ className="text-sm text-danger hover:underline disabled:opacity-55"
117
+ >
118
+ Remove
119
+ </button>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ )}
124
+
125
+ <form onSubmit={handleAdd} className="flex gap-2">
126
+ <input
127
+ type="text"
128
+ aria-label="Sender address"
129
+ value={newSender}
130
+ onChange={(e) => setNewSender(e.target.value)}
131
+ placeholder="sender@example.com"
132
+ className="flex-1 text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
133
+ />
134
+ <select
135
+ aria-label="Classify as"
136
+ value={newClassifyAs}
137
+ onChange={(e) => setNewClassifyAs(e.target.value as MessageClassification)}
138
+ className="text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary"
139
+ >
140
+ <option value="other">Other</option>
141
+ <option value="focused">Focused</option>
142
+ </select>
143
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
144
+ Add
145
+ </Button>
146
+ </form>
147
+ </div>
148
+ </div>
149
+ );
150
+ }