@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
@@ -24,7 +24,7 @@ export default function MessageDetailPage(props: MailShellProps & { params: { ui
24
24
  }
25
25
 
26
26
  function MessageDetailContent({ uid }: { uid: string }) {
27
- const { folders, mailboxUid } = useMailShell();
27
+ const { mailboxFolders, mailboxUid } = useMailShell();
28
28
  const [message, setMessage] = useState<Message | null>(null);
29
29
  const [loading, setLoading] = useState(true);
30
30
  const [error, setError] = useState<string | null>(null);
@@ -40,13 +40,25 @@ function MessageDetailContent({ uid }: { uid: string }) {
40
40
  }, [uid]);
41
41
 
42
42
  // See `apps/www/index.tsx`'s identical effect's own doc comment - a failure here just hides the
43
- // Labels control rather than blocking the rest of the page. No `mailboxUid` guard needed - `MailShell`
44
- // never renders this component at all until `mailboxUid` has resolved.
43
+ // Labels control rather than blocking the rest of the page. Keyed on the message's own mailbox, not the
44
+ // shell's ambient one: this route is reached from aggregate/search rows too, where they can differ.
45
+ const messageMailboxUid = message?.mailboxUid;
45
46
  useEffect(() => {
46
- listLabels(mailboxUid!, { limit: 200 })
47
- .then(setLabels)
48
- .catch(() => setLabels([]));
49
- }, [mailboxUid]);
47
+ if (!messageMailboxUid) {
48
+ return;
49
+ }
50
+ let cancelled = false;
51
+ listLabels(messageMailboxUid, { limit: 200 })
52
+ .then((result) => {
53
+ if (!cancelled) {
54
+ setLabels(result);
55
+ }
56
+ })
57
+ .catch(() => undefined);
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ }, [messageMailboxUid]);
50
62
 
51
63
  const attachments = useMessageAttachments(message);
52
64
  useMarkMessageRead(message, setMessage);
@@ -58,6 +70,10 @@ function MessageDetailContent({ uid }: { uid: string }) {
58
70
  return <Alert>{error ?? "Message not found."}</Alert>;
59
71
  }
60
72
 
73
+ // Looked up by the message's own mailboxUid, not the shell's ambient `mailboxUid` - the mobile detail
74
+ // route reaches here from an aggregate ("All Inboxes" etc.) list row too, where there's no single
75
+ // selected mailbox to fall back to.
76
+ const folders = mailboxFolders.find((mf) => mf.mailbox.uid === message.mailboxUid)?.folders ?? [];
61
77
  const backHref = `/?mailboxUid=${encodeURIComponent(message.mailboxUid)}&folderUid=${encodeURIComponent(message.folderUid)}`;
62
78
  const isSentItems = folders.find((f) => f.uid === message.folderUid)?.type === "sent_items";
63
79
  const isOutbox = folders.find((f) => f.uid === message.folderUid)?.type === "outbox";
@@ -1,129 +1,134 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { FormEvent, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
8
- import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
9
- import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
-
13
- const INPUT_CLASS =
14
- "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
-
16
- export type SettingsAutoReplyPageProps = Omit<SettingsShellProps, "active">;
17
-
18
- export default function SettingsAutoReplyPage(props: SettingsAutoReplyPageProps) {
19
- return (
20
- <SettingsShell {...props} active="auto-reply">
21
- <AutoReplyContent />
22
- </SettingsShell>
23
- );
24
- }
25
-
26
- function AutoReplyContent() {
27
- const { mailboxUid, mailboxes } = useSettingsShell();
28
- // `SettingsShell` only ever renders its children once `mailboxes` has loaded and `mailboxUid` has
29
- // resolved to one of them (a mailbox-less caller sees `MailboxProvisioning` instead) — this lookup
30
- // can't miss, same established non-null pattern as `apps/www/contacts/index.tsx`'s `mailboxUid!`.
31
- const mailbox = mailboxes.find((mb) => mb.uid === mailboxUid)!;
32
-
33
- const [oofEnabled, setOofEnabled] = useState(mailbox.oofEnabled ?? false);
34
- const [oofMessage, setOofMessage] = useState(mailbox.oofMessage ?? "");
35
- const [oofStartTime, setOofStartTime] = useState(mailbox.oofStartTime ? toDatetimeLocal(mailbox.oofStartTime) : "");
36
- const [oofEndTime, setOofEndTime] = useState(mailbox.oofEndTime ? toDatetimeLocal(mailbox.oofEndTime) : "");
37
- const [error, setError] = useState<string | null>(null);
38
- const [saving, setSaving] = useState(false);
39
- const [saved, setSaved] = useState(false);
40
-
41
- async function handleSubmit(e: FormEvent) {
42
- e.preventDefault();
43
- setError(null);
44
- setSaved(false);
45
- setSaving(true);
46
- try {
47
- await updateMailbox({
48
- uid: mailbox.uid,
49
- version: mailbox.version,
50
- oofEnabled,
51
- oofMessage,
52
- oofStartTime: oofStartTime ? new Date(oofStartTime).toISOString() : undefined,
53
- oofEndTime: oofEndTime ? new Date(oofEndTime).toISOString() : undefined,
54
- });
55
- setSaved(true);
56
- } catch (err) {
57
- setError(err instanceof ApiRequestError ? err.message : "Could not save automatic reply settings.");
58
- } finally {
59
- setSaving(false);
60
- }
61
- }
62
-
63
- return (
64
- <div className="flex-1 min-w-0 overflow-y-auto p-6">
65
- <div className="max-w-xl">
66
- <h1 className="text-lg font-bold tracking-tight mb-1">Automatic Replies</h1>
67
- <p className="text-sm text-text-muted mb-4">
68
- Send an automatic reply to anyone who emails {mailbox.displayName} while you&rsquo;re away. A
69
- calendar event with its own automatic reply enabled takes over for its own start/end window.
70
- </p>
71
-
72
- {error && <Alert>{error}</Alert>}
73
- {saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
74
-
75
- <form onSubmit={handleSubmit} className="flex flex-col gap-4">
76
- <label className="flex items-center gap-2 text-sm font-medium">
77
- <input type="checkbox" checked={oofEnabled} onChange={(e) => setOofEnabled(e.target.checked)} />
78
- Automatic replies are on
79
- </label>
80
-
81
- {oofEnabled && (
82
- <>
83
- <label className="flex flex-col gap-1.5 text-sm">
84
- <span className="font-semibold">Message</span>
85
- <textarea
86
- aria-label="Automatic reply message"
87
- className={INPUT_CLASS}
88
- rows={5}
89
- value={oofMessage}
90
- onChange={(e) => setOofMessage(e.target.value)}
91
- />
92
- </label>
93
-
94
- <div className="grid grid-cols-2 gap-3">
95
- <label className="flex flex-col gap-1.5 text-sm">
96
- <span className="font-semibold">Start (optional)</span>
97
- <input
98
- type="datetime-local"
99
- aria-label="Automatic reply start"
100
- className={INPUT_CLASS}
101
- value={oofStartTime}
102
- onChange={(e) => setOofStartTime(e.target.value)}
103
- />
104
- </label>
105
- <label className="flex flex-col gap-1.5 text-sm">
106
- <span className="font-semibold">End (optional)</span>
107
- <input
108
- type="datetime-local"
109
- aria-label="Automatic reply end"
110
- className={INPUT_CLASS}
111
- value={oofEndTime}
112
- onChange={(e) => setOofEndTime(e.target.value)}
113
- />
114
- </label>
115
- </div>
116
- <p className="text-xs text-text-muted -mt-2">Leave both blank to stay on indefinitely until turned off.</p>
117
- </>
118
- )}
119
-
120
- <div>
121
- <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
122
- Save
123
- </Button>
124
- </div>
125
- </form>
126
- </div>
127
- </div>
128
- );
129
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { FormEvent, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
8
+ import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
9
+ import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
+
13
+ const INPUT_CLASS =
14
+ "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
+
16
+ export type SettingsAutoReplyPageProps = Omit<SettingsShellProps, "active">;
17
+
18
+ export default function SettingsAutoReplyPage(props: SettingsAutoReplyPageProps) {
19
+ return (
20
+ <SettingsShell {...props} active="auto-reply">
21
+ <AutoReplyContent />
22
+ </SettingsShell>
23
+ );
24
+ }
25
+
26
+ function AutoReplyContent() {
27
+ const { mailboxUid, mailboxes } = useSettingsShell();
28
+ // `SettingsShell` only ever renders its children once `mailboxes` has loaded and `mailboxUid` has
29
+ // resolved to one of them (a mailbox-less caller sees `MailboxProvisioning` instead) — this lookup
30
+ // can't miss, same established non-null pattern as `apps/www/contacts/index.tsx`'s `mailboxUid!`.
31
+ const mailbox = mailboxes.find((mb) => mb.uid === mailboxUid)!;
32
+
33
+ const [oofEnabled, setOofEnabled] = useState(mailbox.oofEnabled ?? false);
34
+ const [oofMessage, setOofMessage] = useState(mailbox.oofMessage ?? "");
35
+ const [oofStartTime, setOofStartTime] = useState(mailbox.oofStartTime ? toDatetimeLocal(mailbox.oofStartTime) : "");
36
+ const [oofEndTime, setOofEndTime] = useState(mailbox.oofEndTime ? toDatetimeLocal(mailbox.oofEndTime) : "");
37
+ // `SettingsShell`'s `mailboxes` is fetched once - every save bumps the server's version, so a second
38
+ // save without a reload must send the version the previous save returned, not the stale original.
39
+ const [version, setVersion] = useState(mailbox.version);
40
+ const [error, setError] = useState<string | null>(null);
41
+ const [saving, setSaving] = useState(false);
42
+ const [saved, setSaved] = useState(false);
43
+
44
+ async function handleSubmit(e: FormEvent) {
45
+ e.preventDefault();
46
+ setError(null);
47
+ setSaved(false);
48
+ setSaving(true);
49
+ try {
50
+ const updated = await updateMailbox({
51
+ uid: mailbox.uid,
52
+ version,
53
+ oofEnabled,
54
+ oofMessage,
55
+ // `null` (not omitted) clears a previously saved window - an omitted field is left as-is.
56
+ oofStartTime: oofStartTime ? new Date(oofStartTime).toISOString() : null,
57
+ oofEndTime: oofEndTime ? new Date(oofEndTime).toISOString() : null,
58
+ });
59
+ setVersion(updated.version);
60
+ setSaved(true);
61
+ } catch (err) {
62
+ setError(err instanceof ApiRequestError ? err.message : "Could not save automatic reply settings.");
63
+ } finally {
64
+ setSaving(false);
65
+ }
66
+ }
67
+
68
+ return (
69
+ <div className="flex-1 min-w-0 overflow-y-auto p-6">
70
+ <div className="max-w-xl">
71
+ <h1 className="text-lg font-bold tracking-tight mb-1">Automatic Replies</h1>
72
+ <p className="text-sm text-text-muted mb-4">
73
+ Send an automatic reply to anyone who emails {mailbox.displayName} while you&rsquo;re away. A
74
+ calendar event with its own automatic reply enabled takes over for its own start/end window.
75
+ </p>
76
+
77
+ {error && <Alert>{error}</Alert>}
78
+ {saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
79
+
80
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
81
+ <label className="flex items-center gap-2 text-sm font-medium">
82
+ <input type="checkbox" checked={oofEnabled} onChange={(e) => setOofEnabled(e.target.checked)} />
83
+ Automatic replies are on
84
+ </label>
85
+
86
+ {oofEnabled && (
87
+ <>
88
+ <label className="flex flex-col gap-1.5 text-sm">
89
+ <span className="font-semibold">Message</span>
90
+ <textarea
91
+ aria-label="Automatic reply message"
92
+ className={INPUT_CLASS}
93
+ rows={5}
94
+ value={oofMessage}
95
+ onChange={(e) => setOofMessage(e.target.value)}
96
+ />
97
+ </label>
98
+
99
+ <div className="grid grid-cols-2 gap-3">
100
+ <label className="flex flex-col gap-1.5 text-sm">
101
+ <span className="font-semibold">Start (optional)</span>
102
+ <input
103
+ type="datetime-local"
104
+ aria-label="Automatic reply start"
105
+ className={INPUT_CLASS}
106
+ value={oofStartTime}
107
+ onChange={(e) => setOofStartTime(e.target.value)}
108
+ />
109
+ </label>
110
+ <label className="flex flex-col gap-1.5 text-sm">
111
+ <span className="font-semibold">End (optional)</span>
112
+ <input
113
+ type="datetime-local"
114
+ aria-label="Automatic reply end"
115
+ className={INPUT_CLASS}
116
+ value={oofEndTime}
117
+ onChange={(e) => setOofEndTime(e.target.value)}
118
+ />
119
+ </label>
120
+ </div>
121
+ <p className="text-xs text-text-muted -mt-2">Leave both blank to stay on indefinitely until turned off.</p>
122
+ </>
123
+ )}
124
+
125
+ <div>
126
+ <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
127
+ Save
128
+ </Button>
129
+ </div>
130
+ </form>
131
+ </div>
132
+ </div>
133
+ );
134
+ }