@rapidmx/web-client 0.9.0 → 0.10.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.
- package/README.md +192 -88
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactsToolbar.tsx +125 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +15 -3
- package/apps/shared/components/layout/AppShell.tsx +411 -288
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
- package/apps/shared/components/layout/UserMenu.tsx +303 -185
- package/apps/shared/components/mail/ConversationList.tsx +269 -262
- package/apps/shared/components/mail/ConversationThreadPane.tsx +475 -434
- package/apps/shared/components/mail/LazyReadingPane.tsx +115 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +235 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1458 -1387
- package/apps/shared/components/mail/NewMailToasts.tsx +141 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +279 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1842 -1736
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +476 -472
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +252 -240
- package/apps/shared/components/tasks/layout/TasksShell.tsx +15 -3
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +302 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/useMailConnection.ts +150 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +232 -224
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +163 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/styles/app.css +28 -10
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +35 -0
- package/apps/www/calendar/index.tsx +55 -19
- package/apps/www/contacts/[uid].tsx +112 -107
- package/apps/www/contacts/index.tsx +584 -567
- package/apps/www/index.tsx +2280 -1917
- package/apps/www/messages/[uid].tsx +106 -101
- package/apps/www/settings/auto-reply/index.tsx +4 -1
- package/apps/www/settings/encryption/index.tsx +1252 -1249
- package/apps/www/settings/filters/[uid].tsx +4 -1
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +138 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/read-receipts/index.tsx +4 -1
- package/apps/www/settings/sharing/index.tsx +277 -274
- package/apps/www/settings/signatures/[uid].tsx +170 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +134 -129
- package/apps/www/tasks/index.tsx +18 -2
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +7 -4
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +15 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +29 -4
- package/dist/apps/shared/components/layout/AppShell.js +73 -14
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
- package/dist/apps/shared/components/layout/UserMenu.d.ts +19 -3
- package/dist/apps/shared/components/layout/UserMenu.js +52 -5
- package/dist/apps/shared/components/mail/ConversationList.js +4 -11
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +52 -27
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +82 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +4 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +7 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +83 -34
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +19 -0
- package/dist/apps/shared/components/mail/NewMailToasts.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +30 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +71 -5
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +98 -22
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +12 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -96
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +15 -5
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +15 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +212 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +55 -0
- package/dist/apps/shared/mail/useMailConnection.js +96 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +13 -6
- package/dist/apps/shared/mail/useMailLiveUpdates.js +28 -26
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +41 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +110 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/styles/app.css +28 -10
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +29 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +39 -8
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +17 -4
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +301 -35
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +7 -4
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +3 -1
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +6 -2
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +3 -1
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +3 -1
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +3 -1
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +6 -2
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +15 -3
- package/package.json +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How an unread message looks in every list of mail (the message list, the conversation list and its child rows, the
|
|
10
|
+
* thread pane's message headers, the folder badges), in one place so they can't drift apart. Modelled on Outlook:
|
|
11
|
+
*
|
|
12
|
+
* **Unread**: an accent bar down the row's left edge, a faint accent tint on the row, the sender and subject in bold and the
|
|
13
|
+
* date in the accent colour - plus a visually-hidden "Unread" in the row's text, so it never relies on colour alone.
|
|
14
|
+
*
|
|
15
|
+
* **Read**: normal weight, the sender and preview muted.
|
|
16
|
+
*
|
|
17
|
+
* **Hover, selected and focused** are their own states and look nothing like unread: hover is a plain grey (an accent tint on
|
|
18
|
+
* an unread row, so the row doesn't seem to lose its state), the open row has a stronger accent fill and an inset outline, and
|
|
19
|
+
* keyboard focus is a solid outline drawn inside the row.
|
|
20
|
+
*
|
|
21
|
+
* All of it is built from the theme's own tokens (`primary`, `text`, `text-muted`, `surface-alt`), so it follows light and
|
|
22
|
+
* dark mode and a branding stylesheet's palette.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Whether a message is unread: `flags.read` isn't `true`. A message with no `read` flag at all counts as unread. */
|
|
26
|
+
export function isUnread(message: Pick<Message, "flags"> | undefined | null): boolean {
|
|
27
|
+
return !!message && message.flags.read !== true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The classes of a row's outer element (`li`, or the row's wrapping `div`): the positioning the bar needs, and its one
|
|
32
|
+
* background - the open row's accent fill and inset outline, else an unread row's faint tint (a little stronger on hover),
|
|
33
|
+
* else a read row's plain grey hover. One element owns all of it so the states can't fight over the same property.
|
|
34
|
+
*/
|
|
35
|
+
export function rowClass(state: { unread: boolean; selected: boolean }, extra = ""): string {
|
|
36
|
+
return [
|
|
37
|
+
"relative border-b border-border",
|
|
38
|
+
state.selected
|
|
39
|
+
? "bg-primary/20 ring-1 ring-inset ring-primary/50"
|
|
40
|
+
: state.unread
|
|
41
|
+
? "bg-primary/[0.07] hover:bg-primary/10"
|
|
42
|
+
: "hover:bg-surface-alt",
|
|
43
|
+
extra,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Keyboard focus on a row's button: a solid outline drawn inside the row, unlike anything unread/selected/hover does. */
|
|
50
|
+
export const ROW_FOCUS_CLASS = "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary";
|
|
51
|
+
|
|
52
|
+
/** The sender line: bold and full-strength while unread, normal and muted once read. */
|
|
53
|
+
export function senderClass(unread: boolean): string {
|
|
54
|
+
return unread ? "font-bold text-text" : "font-normal text-text-muted";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The subject line. */
|
|
58
|
+
export function subjectClass(unread: boolean): string {
|
|
59
|
+
return unread ? "font-semibold text-text" : "font-normal text-text";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The date beside the sender: the accent colour while unread. */
|
|
63
|
+
export function dateClass(unread: boolean): string {
|
|
64
|
+
return unread ? "font-semibold text-primary-dark" : "font-normal text-text-muted";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The accent bar down a row's left edge - only for an unread row. The row's outer element must be `relative`. */
|
|
68
|
+
export function UnreadBar({ unread }: { unread: boolean }) {
|
|
69
|
+
return unread ? <span aria-hidden="true" data-unread-bar className="absolute inset-y-0 left-0 w-1 bg-primary" /> : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** "Unread." in the row's text for assistive technology (and for anything that reads the DOM without styles). */
|
|
73
|
+
export function UnreadLabel({ unread }: { unread: boolean }) {
|
|
74
|
+
return unread ? <span className="sr-only">Unread. </span> : null;
|
|
75
|
+
}
|
|
@@ -1,240 +1,252 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
-
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
-
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
|
-
import { Mailbox, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
-
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
-
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
-
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ id: "
|
|
26
|
-
{ id: "
|
|
27
|
-
{ id: "
|
|
28
|
-
{ id: "
|
|
29
|
-
{ id: "
|
|
30
|
-
{ id: "
|
|
31
|
-
{ id: "
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const [
|
|
93
|
-
const [
|
|
94
|
-
const [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
</
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
</
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
|
+
import { Mailbox, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
+
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
+
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
14
|
+
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
|
+
import { mergePluginNavItems, PluginNav } from "../../../plugins/pluginNav.js";
|
|
16
|
+
|
|
17
|
+
export interface SettingsSectionDef {
|
|
18
|
+
id: string;
|
|
19
|
+
href: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Every settings section with its own sidebar entry. */
|
|
24
|
+
export const SETTINGS_SECTIONS: SettingsSectionDef[] = [
|
|
25
|
+
{ id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
|
|
26
|
+
{ id: "filters", href: "/settings/filters", label: "Mail Filters" },
|
|
27
|
+
{ id: "signatures", href: "/settings/signatures", label: "Signatures" },
|
|
28
|
+
{ id: "labels", href: "/settings/labels", label: "Labels" },
|
|
29
|
+
{ id: "read-receipts", href: "/settings/read-receipts", label: "Read Receipts" },
|
|
30
|
+
{ id: "encryption", href: "/settings/encryption", label: "Encryption" },
|
|
31
|
+
{ id: "sharing", href: "/settings/sharing", label: "Sharing" },
|
|
32
|
+
{ id: "privacy", href: "/settings/privacy", label: "Privacy & Data" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** A `SETTINGS_SECTIONS` id, or a plugin's `settingsSections` item id (see `PluginNav`). */
|
|
36
|
+
export type SettingsSectionId = string;
|
|
37
|
+
|
|
38
|
+
/** `SETTINGS_SECTIONS` followed by the plugins' `settingsSections` items, core ids winning - see
|
|
39
|
+
* `mergePluginNavItems`. */
|
|
40
|
+
export function settingsSections(pluginNav?: PluginNav): SettingsSectionDef[] {
|
|
41
|
+
return mergePluginNavItems<SettingsSectionDef>(SETTINGS_SECTIONS, pluginNav?.settingsSections, ({ id, href, label }) => ({
|
|
42
|
+
id,
|
|
43
|
+
href,
|
|
44
|
+
label,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SettingsShellProps extends Omit<AppShellProps, "active"> {
|
|
49
|
+
/** Which entry in `SETTINGS_SECTIONS` (or plugin `settingsSections` item) is highlighted in this shell's own sidebar — distinct from
|
|
50
|
+
* `AppShell`'s own `active` (always hardcoded to `"settings"` below), since Settings isn't a rail
|
|
51
|
+
* icon (see `AppShell.tsx`'s `AppShellApp` doc comment) but still needs its own section
|
|
52
|
+
* highlighting. Each settings page passes its own section id explicitly, the same "gotcha" this
|
|
53
|
+
* plan already calls out for every shell's `active` prop. */
|
|
54
|
+
active: SettingsSectionId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SettingsShellContextValue {
|
|
58
|
+
/** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox) —
|
|
59
|
+
* every settings section so far (Automatic Replies) edits mailbox-scoped fields. */
|
|
60
|
+
mailboxUid?: string;
|
|
61
|
+
mailboxes: Mailbox[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SettingsShellContext = createContext<SettingsShellContextValue>({ mailboxes: [] });
|
|
65
|
+
|
|
66
|
+
/** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
|
|
67
|
+
export function useSettingsShell(): SettingsShellContextValue {
|
|
68
|
+
return useContext(SettingsShellContext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type Status = "checking" | "error" | "ready";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The Settings area's shell — reached via a `UserMenu` entry rather than a 5th `AppShell` icon (per
|
|
75
|
+
* this plan's own confirmed decision), so it always passes `active="settings"` to `AppShell` regardless
|
|
76
|
+
* of which section is showing. Unlike `ContactsShell`/`TasksShell` (whose sidebar is just a mailbox
|
|
77
|
+
* switcher, since each has exactly one well-known folder), this shell's sidebar is a real list of
|
|
78
|
+
* settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
|
|
79
|
+
* app rail, since this framework has no client-side router.
|
|
80
|
+
*/
|
|
81
|
+
export default function SettingsShell({
|
|
82
|
+
active,
|
|
83
|
+
userUid,
|
|
84
|
+
authServerUrl,
|
|
85
|
+
impersonating,
|
|
86
|
+
impersonationBaseUrl,
|
|
87
|
+
trusted,
|
|
88
|
+
trustedRoles,
|
|
89
|
+
pluginNav,
|
|
90
|
+
children,
|
|
91
|
+
}: PropsWithChildren<SettingsShellProps>) {
|
|
92
|
+
const [status, setStatus] = useState<Status>("checking");
|
|
93
|
+
const [error, setError] = useState<string | null>(null);
|
|
94
|
+
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
|
|
95
|
+
const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
|
|
96
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
97
|
+
|
|
98
|
+
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
99
|
+
// effect, not during render, so the server render and the hydrating render agree.
|
|
100
|
+
const search = useLocationSearch();
|
|
101
|
+
const navigate = useNavigate();
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
104
|
+
}, [search]);
|
|
105
|
+
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
106
|
+
// with the page - is closed here.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
setDrawerOpen(false);
|
|
109
|
+
}, [search]);
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!userUid) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
listMailboxes({ limit: 100 })
|
|
116
|
+
.then((result) => {
|
|
117
|
+
setMailboxes(result);
|
|
118
|
+
setStatus("ready");
|
|
119
|
+
})
|
|
120
|
+
.catch((err) => {
|
|
121
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
|
|
122
|
+
setStatus("error");
|
|
123
|
+
});
|
|
124
|
+
}, [userUid]);
|
|
125
|
+
|
|
126
|
+
const mailboxUid: string | undefined =
|
|
127
|
+
(requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
128
|
+
mailboxes[0]?.uid;
|
|
129
|
+
|
|
130
|
+
const contextValue = useMemo<SettingsShellContextValue>(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
|
|
131
|
+
|
|
132
|
+
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
133
|
+
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
134
|
+
if (userUid && status === "ready" && !mailboxUid) {
|
|
135
|
+
return <MailboxProvisioning />;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let inner: ReactNode = null;
|
|
139
|
+
if (userUid && status === "checking") {
|
|
140
|
+
inner = (
|
|
141
|
+
<div className="w-56 shrink-0 border-r border-border flex flex-col gap-4 p-3">
|
|
142
|
+
<Skeleton height="h-9" />
|
|
143
|
+
<SkeletonList count={3} />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
} else if (userUid && status === "error") {
|
|
147
|
+
inner = (
|
|
148
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
149
|
+
<div className="w-full max-w-md">
|
|
150
|
+
<Alert>{error}</Alert>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
} else if (userUid && status === "ready") {
|
|
155
|
+
const sections = settingsSections(pluginNav);
|
|
156
|
+
// A function, not a plain JSX constant — same reasoning as ContactsShell's identical comment:
|
|
157
|
+
// rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously mounted, so the
|
|
158
|
+
// `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
|
|
159
|
+
const sidebarContent = (idPrefix: string) => (
|
|
160
|
+
<>
|
|
161
|
+
{mailboxes.length > 1 && (
|
|
162
|
+
<div>
|
|
163
|
+
<label
|
|
164
|
+
className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
|
|
165
|
+
htmlFor={`${idPrefix}-settings-mailbox-switcher`}
|
|
166
|
+
>
|
|
167
|
+
Mailbox
|
|
168
|
+
</label>
|
|
169
|
+
<select
|
|
170
|
+
id={`${idPrefix}-settings-mailbox-switcher`}
|
|
171
|
+
className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
|
|
172
|
+
value={mailboxUid}
|
|
173
|
+
onChange={(e) => {
|
|
174
|
+
// Every core settings page passes its own section id (see
|
|
175
|
+
// `SettingsShellProps.active`), but a plugin page's section is missing when the
|
|
176
|
+
// server didn't send its nav item, so that falls back to the current path.
|
|
177
|
+
const href = sections.find((s) => s.id === active)?.href ?? window.location.pathname;
|
|
178
|
+
navigate(`${href}?mailboxUid=${encodeURIComponent(e.target.value)}`);
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{mailboxes.map((mb) => (
|
|
182
|
+
<option key={mb.uid} value={mb.uid}>
|
|
183
|
+
{mb.displayName}
|
|
184
|
+
{mb.ownerUserUid ? "" : " (shared)"}
|
|
185
|
+
</option>
|
|
186
|
+
))}
|
|
187
|
+
</select>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
<nav aria-label="Settings sections" className="flex flex-col gap-0.5">
|
|
191
|
+
{sections.map((section) => (
|
|
192
|
+
<a
|
|
193
|
+
key={section.id}
|
|
194
|
+
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
195
|
+
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
196
|
+
// `<MailboxProvisioning />` above instead (and TypeScript's own narrowing of
|
|
197
|
+
// this `const` already reflects that, no `!` needed) — so no fallback is
|
|
198
|
+
// needed here, matching this plan's established "dead guard the UI
|
|
199
|
+
// structurally can't trigger" removal precedent (e.g.
|
|
200
|
+
// `DomainDetailContent.handleVerify`).
|
|
201
|
+
href={`${section.href}?mailboxUid=${encodeURIComponent(mailboxUid)}`}
|
|
202
|
+
aria-current={section.id === active ? "page" : undefined}
|
|
203
|
+
className={[
|
|
204
|
+
"block px-2.5 py-1.5 rounded-sm text-sm",
|
|
205
|
+
section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
|
|
206
|
+
].join(" ")}
|
|
207
|
+
>
|
|
208
|
+
{section.label}
|
|
209
|
+
</a>
|
|
210
|
+
))}
|
|
211
|
+
</nav>
|
|
212
|
+
</>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
inner = (
|
|
216
|
+
<>
|
|
217
|
+
<aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
|
|
218
|
+
{sidebarContent("desktop")}
|
|
219
|
+
</aside>
|
|
220
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Settings">
|
|
221
|
+
<div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
|
|
222
|
+
</Drawer>
|
|
223
|
+
<main className="flex-1 min-w-0 flex flex-col">
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
className="md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
227
|
+
aria-label="Open settings menu"
|
|
228
|
+
onClick={() => setDrawerOpen(true)}
|
|
229
|
+
>
|
|
230
|
+
<HiOutlineBars3 size={20} aria-hidden="true" />
|
|
231
|
+
</button>
|
|
232
|
+
<SettingsShellContext.Provider value={contextValue}>{children}</SettingsShellContext.Provider>
|
|
233
|
+
</main>
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<AppShell
|
|
240
|
+
active="settings"
|
|
241
|
+
userUid={userUid}
|
|
242
|
+
authServerUrl={authServerUrl}
|
|
243
|
+
impersonating={impersonating}
|
|
244
|
+
impersonationBaseUrl={impersonationBaseUrl}
|
|
245
|
+
trusted={trusted}
|
|
246
|
+
trustedRoles={trustedRoles}
|
|
247
|
+
pluginNav={pluginNav}
|
|
248
|
+
>
|
|
249
|
+
{inner}
|
|
250
|
+
</AppShell>
|
|
251
|
+
);
|
|
252
|
+
}
|