@rapidmx/web-client 0.3.0 → 0.4.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/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/index.tsx +100 -82
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
- package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +40 -4
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactForm.tsx +38 -4
- package/apps/shared/components/layout/AppShell.tsx +198 -169
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
- package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
- package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +179 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +116 -0
- package/apps/shared/search/localIndexSchema.ts +227 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexVFS.ts +235 -0
- package/apps/shared/search/localIndexWorker.ts +464 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +31 -21
- package/apps/www/contacts/index.tsx +33 -6
- package/apps/www/index.tsx +661 -106
- package/apps/www/messages/[uid].tsx +5 -1
- package/apps/www/settings/encryption/index.tsx +63 -1
- package/apps/www/settings/sharing/index.tsx +271 -0
- package/apps/www/tasks/index.tsx +53 -4
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/index.js +14 -1
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +16 -4
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
- package/dist/apps/shared/components/layout/AppShell.js +30 -7
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
- package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +153 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
- package/dist/apps/shared/search/localIndexSchema.js +179 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexVFS.js +230 -0
- package/dist/apps/shared/search/localIndexWorker.js +341 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +14 -12
- package/dist/apps/www/contacts/index.js +15 -6
- package/dist/apps/www/index.js +496 -94
- package/dist/apps/www/messages/[uid].js +5 -1
- package/dist/apps/www/settings/encryption/index.js +30 -2
- package/dist/apps/www/settings/sharing/index.js +128 -0
- package/dist/apps/www/tasks/index.js +27 -5
- package/package.json +3 -2
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
createContact,
|
|
14
14
|
updateContact,
|
|
15
15
|
} from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
16
|
+
import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
16
17
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
17
18
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
18
19
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
20
|
+
import { findWellKnownFolderUid } from "../../mail/findWellKnownFolderUid.js";
|
|
19
21
|
|
|
20
22
|
const INPUT_CLASS =
|
|
21
23
|
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -24,8 +26,12 @@ const SELECT_CLASS =
|
|
|
24
26
|
|
|
25
27
|
export interface ContactFormProps {
|
|
26
28
|
contact?: Contact;
|
|
29
|
+
/** Create mode: the default mailbox (and its Contacts folder) to create in. */
|
|
27
30
|
mailboxUid?: string;
|
|
28
31
|
folderUid?: string;
|
|
32
|
+
/** Create mode: every mailbox the contact could be created in - with more than one, a Mailbox selector
|
|
33
|
+
* is shown. Choosing a mailbox other than `mailboxUid` looks up that mailbox's own Contacts folder. */
|
|
34
|
+
mailboxes?: Mailbox[];
|
|
29
35
|
onSaved: (contact: Contact) => void;
|
|
30
36
|
onCancel: () => void;
|
|
31
37
|
}
|
|
@@ -37,7 +43,8 @@ const ADDRESS_KINDS: ContactAddressKind[] = ["home", "work", "other"];
|
|
|
37
43
|
* mobile detail route's own edit mode (`apps/www/contacts/[uid].tsx`) — creation itself stays
|
|
38
44
|
* desktop-and-mobile-inline (see that route's own doc comment on why "new" never gets a dedicated route).
|
|
39
45
|
*/
|
|
40
|
-
export default function ContactForm({ contact, mailboxUid, folderUid, onSaved, onCancel }: ContactFormProps) {
|
|
46
|
+
export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes, onSaved, onCancel }: ContactFormProps) {
|
|
47
|
+
const [targetMailboxUid, setTargetMailboxUid] = useState(mailboxUid);
|
|
41
48
|
const [displayName, setDisplayName] = useState(contact?.displayName ?? "");
|
|
42
49
|
const [givenName, setGivenName] = useState(contact?.givenName ?? "");
|
|
43
50
|
const [surname, setSurname] = useState(contact?.surname ?? "");
|
|
@@ -93,9 +100,18 @@ export default function ContactForm({ contact, mailboxUid, folderUid, onSaved, o
|
|
|
93
100
|
phones,
|
|
94
101
|
addresses: address ? [address] : [],
|
|
95
102
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
let saved: Contact;
|
|
104
|
+
if (contact) {
|
|
105
|
+
saved = await updateContact({ uid: contact.uid, version: contact.version, mailboxUid: contact.mailboxUid, folderUid: contact.folderUid, ...input });
|
|
106
|
+
} else {
|
|
107
|
+
const targetFolderUid =
|
|
108
|
+
targetMailboxUid === mailboxUid ? folderUid : await findWellKnownFolderUid(targetMailboxUid as string, "contacts");
|
|
109
|
+
if (!targetFolderUid) {
|
|
110
|
+
setError("That mailbox has no Contacts folder.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
saved = await createContact({ mailboxUid: targetMailboxUid as string, folderUid: targetFolderUid, ...input });
|
|
114
|
+
}
|
|
99
115
|
onSaved(saved);
|
|
100
116
|
} catch (err) {
|
|
101
117
|
setError(err instanceof ApiRequestError ? err.message : "Could not save this contact.");
|
|
@@ -110,6 +126,24 @@ export default function ContactForm({ contact, mailboxUid, folderUid, onSaved, o
|
|
|
110
126
|
|
|
111
127
|
{error && <Alert>{error}</Alert>}
|
|
112
128
|
|
|
129
|
+
{!contact && mailboxes && mailboxes.length > 1 && (
|
|
130
|
+
<FormField label="Mailbox" htmlFor="contact-mailbox">
|
|
131
|
+
<select
|
|
132
|
+
id="contact-mailbox"
|
|
133
|
+
className={`${SELECT_CLASS} w-full`}
|
|
134
|
+
value={targetMailboxUid}
|
|
135
|
+
onChange={(e) => setTargetMailboxUid(e.target.value)}
|
|
136
|
+
>
|
|
137
|
+
{mailboxes.map((mb) => (
|
|
138
|
+
<option key={mb.uid} value={mb.uid}>
|
|
139
|
+
{mb.displayName}
|
|
140
|
+
{mb.ownerUserUid ? "" : " (shared)"}
|
|
141
|
+
</option>
|
|
142
|
+
))}
|
|
143
|
+
</select>
|
|
144
|
+
</FormField>
|
|
145
|
+
)}
|
|
146
|
+
|
|
113
147
|
<FormField label="Display name" htmlFor="contact-displayName">
|
|
114
148
|
<input
|
|
115
149
|
id="contact-displayName"
|
|
@@ -1,169 +1,198 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import "../../styles/app.css";
|
|
6
|
-
import React, { PropsWithChildren, useState } from "react";
|
|
7
|
-
import type { IconType } from "react-icons";
|
|
8
|
-
import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlineUsers } from "react-icons/hi2";
|
|
9
|
-
import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ id: "
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import "../../styles/app.css";
|
|
6
|
+
import React, { PropsWithChildren, useState, useEffect } from "react";
|
|
7
|
+
import type { IconType } from "react-icons";
|
|
8
|
+
import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlineUsers } from "react-icons/hi2";
|
|
9
|
+
import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
|
|
10
|
+
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
11
|
+
import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
12
|
+
import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
|
|
13
|
+
import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeout.js";
|
|
14
|
+
import ComposeProvider from "../mail/compose/ComposeContext.js";
|
|
15
|
+
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
16
|
+
import { BrandingFooter, BrandingHeader } from "./BrandingChrome.js";
|
|
17
|
+
import UserMenu from "./UserMenu.js";
|
|
18
|
+
import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
|
|
19
|
+
import { destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
|
|
20
|
+
|
|
21
|
+
export type AppShellApp = "mail" | "calendar" | "contacts" | "tasks";
|
|
22
|
+
|
|
23
|
+
/** `"settings"` is a valid `active` value but deliberately has no entry in `APPS` below — Settings is
|
|
24
|
+
* reached via a `UserMenu` item, not a 5th rail icon (see `SettingsShell.tsx`), so it highlights no
|
|
25
|
+
* rail/tab icon at all; only the header title (`ACTIVE_LABELS` below) needs to account for it. */
|
|
26
|
+
export type AppShellActive = AppShellApp | "settings";
|
|
27
|
+
|
|
28
|
+
export interface AppShellProps {
|
|
29
|
+
/** Which icon in the rail is highlighted as the current app — `"settings"` highlights none. */
|
|
30
|
+
active: AppShellActive;
|
|
31
|
+
/** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
|
|
32
|
+
userUid?: string;
|
|
33
|
+
/** auth-server's base URL, injected via the route's `fetchProps`. */
|
|
34
|
+
authServerUrl?: string;
|
|
35
|
+
/**
|
|
36
|
+
* `true` when this session is an admin "log in as user" impersonation (a `jwt_impersonator` cookie is
|
|
37
|
+
* present) — see `MailShell`'s original doc comment, unchanged now that this lives here. Drives the
|
|
38
|
+
* "you are viewing as this user — stop impersonating" banner below.
|
|
39
|
+
*/
|
|
40
|
+
impersonating?: boolean;
|
|
41
|
+
/** Where `mailApi.ts`'s `stopImpersonating()` should call — see `MailShell`'s original doc comment. */
|
|
42
|
+
impersonationBaseUrl?: string;
|
|
43
|
+
/** `true` when the caller's JWT carries a trusted role — shows an "Admin" item in the user menu below. */
|
|
44
|
+
trusted?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AppDef {
|
|
48
|
+
id: AppShellApp;
|
|
49
|
+
href: string;
|
|
50
|
+
label: string;
|
|
51
|
+
icon: IconType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const APPS: AppDef[] = [
|
|
55
|
+
{ id: "mail", href: "/", label: "Mail", icon: HiOutlineEnvelope },
|
|
56
|
+
{ id: "calendar", href: "/calendar", label: "Calendar", icon: HiOutlineCalendarDays },
|
|
57
|
+
{ id: "contacts", href: "/contacts", label: "Contacts", icon: HiOutlineUsers },
|
|
58
|
+
{ id: "tasks", href: "/tasks", label: "Tasks", icon: HiOutlineClipboardDocumentList },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** The header title for every valid `active` value — a superset of `APPS`' own labels since `"settings"`
|
|
62
|
+
* has no rail icon (and so no `AppDef`) but still needs a header title. */
|
|
63
|
+
const ACTIVE_LABELS: Record<AppShellActive, string> = {
|
|
64
|
+
mail: "Mail",
|
|
65
|
+
calendar: "Calendar",
|
|
66
|
+
contacts: "Contacts",
|
|
67
|
+
tasks: "Tasks",
|
|
68
|
+
settings: "Settings",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
|
|
73
|
+
* switching apps (full page loads — see `MailShell`'s original doc comment on this framework having no
|
|
74
|
+
* client-side router), a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
75
|
+
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
76
|
+
* the area to the right of the icon rail and below the header.
|
|
77
|
+
*/
|
|
78
|
+
export default function AppShell({
|
|
79
|
+
active,
|
|
80
|
+
userUid,
|
|
81
|
+
authServerUrl,
|
|
82
|
+
impersonating,
|
|
83
|
+
impersonationBaseUrl,
|
|
84
|
+
trusted,
|
|
85
|
+
children,
|
|
86
|
+
}: PropsWithChildren<AppShellProps>) {
|
|
87
|
+
const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
|
|
88
|
+
const { branding, iconSrc } = useBranding();
|
|
89
|
+
|
|
90
|
+
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
91
|
+
// Mounted here, not scoped to Mail/Settings (the only shells that actually read unlocked keys),
|
|
92
|
+
// specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
|
|
93
|
+
useIdleKeyTimeout();
|
|
94
|
+
|
|
95
|
+
// An administrator on a server that hasn't finished first-run setup is sent to the setup wizard. Everyone else
|
|
96
|
+
// gets a 403 from the setup status check (it's admin-only), which is ignored along with any other failure.
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!userUid || impersonating) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
getSetupStatus()
|
|
102
|
+
.then((status) => {
|
|
103
|
+
if (status.required) {
|
|
104
|
+
window.location.href = "/admin/setup";
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.catch(() => undefined);
|
|
108
|
+
}, [userUid, impersonating]);
|
|
109
|
+
|
|
110
|
+
function handleSignOut() {
|
|
111
|
+
// Best-effort, fire-and-forget: the Tier 2 local index MUST be destroyed on explicit logout, the
|
|
112
|
+
// same as unlocked keys themselves (spec §11) - this component has no specific mailboxUid of its
|
|
113
|
+
// own to pass (it's shared by every app, not just Mail), so this destroys every mailbox's index
|
|
114
|
+
// the session has open rather than needing one threaded through. Not awaited - the navigation
|
|
115
|
+
// below shouldn't wait on it, and destroyAllLocalIndexes() never throws either way.
|
|
116
|
+
void destroyAllLocalIndexes();
|
|
117
|
+
window.location.href = authServerUrl ?? "/";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleStopImpersonating() {
|
|
121
|
+
setStoppingImpersonation(true);
|
|
122
|
+
try {
|
|
123
|
+
await stopImpersonating(impersonationBaseUrl ?? "");
|
|
124
|
+
} catch {
|
|
125
|
+
// Navigate either way: a failed call leaves the impersonator cookie (and this banner) exactly as
|
|
126
|
+
// they were, so there's nothing else useful to show — matching this app's other network-error
|
|
127
|
+
// handling, which surfaces via a full reload rather than an inline retry affordance.
|
|
128
|
+
} finally {
|
|
129
|
+
window.location.href = "/admin";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!userUid) {
|
|
134
|
+
return <div className="min-h-screen" />;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
// Mounted here, not scoped to Mail/Settings, for the same reason as useIdleKeyTimeout() above -
|
|
139
|
+
// ComposeWindow's sign/encrypt toggles and MessageDetailPane's encrypted-message view (both Mail)
|
|
140
|
+
// are today's only useUnlockPrompt() callers, but this needs to be available to any app shell.
|
|
141
|
+
<UnlockPromptProvider>
|
|
142
|
+
<ComposeProvider userUid={userUid}>
|
|
143
|
+
<div className="min-h-screen flex flex-col bg-surface-alt">
|
|
144
|
+
<BrandingHeader branding={branding} />
|
|
145
|
+
{impersonating && (
|
|
146
|
+
<div className="h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4">
|
|
147
|
+
<span>
|
|
148
|
+
You are viewing as <strong>{userUid}</strong>.
|
|
149
|
+
</span>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={handleStopImpersonating}
|
|
153
|
+
disabled={stoppingImpersonation}
|
|
154
|
+
className="underline hover:no-underline disabled:opacity-60"
|
|
155
|
+
>
|
|
156
|
+
{stoppingImpersonation ? "Returning to admin…" : "Return to admin"}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
<div className="flex-1 flex min-h-0">
|
|
161
|
+
<nav
|
|
162
|
+
aria-label="Apps"
|
|
163
|
+
className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
|
|
164
|
+
>
|
|
165
|
+
<img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
|
|
166
|
+
{APPS.map(({ id, href, label, icon: Icon }) => (
|
|
167
|
+
<a
|
|
168
|
+
key={id}
|
|
169
|
+
href={href}
|
|
170
|
+
aria-label={label}
|
|
171
|
+
aria-current={id === active ? "page" : undefined}
|
|
172
|
+
title={label}
|
|
173
|
+
className={[
|
|
174
|
+
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
175
|
+
id === active
|
|
176
|
+
? "bg-primary/10 text-primary-dark"
|
|
177
|
+
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
178
|
+
].join(" ")}
|
|
179
|
+
>
|
|
180
|
+
<Icon size={20} aria-hidden="true" />
|
|
181
|
+
</a>
|
|
182
|
+
))}
|
|
183
|
+
</nav>
|
|
184
|
+
<BottomTabBar apps={APPS} active={active} />
|
|
185
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
186
|
+
<header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
|
|
187
|
+
<span className="font-display font-bold text-lg uppercase tracking-wide">{ACTIVE_LABELS[active]}</span>
|
|
188
|
+
<UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} showAdminLink={trusted} showSettingsLink />
|
|
189
|
+
</header>
|
|
190
|
+
<div className="flex-1 flex min-h-0 pb-14 md:pb-0">{children}</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<BrandingFooter branding={branding} />
|
|
194
|
+
</div>
|
|
195
|
+
</ComposeProvider>
|
|
196
|
+
</UnlockPromptProvider>
|
|
197
|
+
);
|
|
198
|
+
}
|