@rapidmx/web-client 0.20.0 → 0.21.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 +381 -381
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -482
- package/apps/admin/domains/[uid].tsx +166 -166
- package/apps/admin/escrow-scopes/[uid].tsx +350 -350
- package/apps/admin/index.tsx +129 -129
- package/apps/admin/mailboxes/[uid].tsx +271 -271
- package/apps/admin/mailboxes/new/index.tsx +30 -30
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +384 -384
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -238
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -194
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +2093 -2093
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -446
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -103
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +463 -461
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -439
- package/apps/shared/components/mail/ConversationList.tsx +332 -332
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -613
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -240
- package/apps/shared/components/mail/MenuButton.tsx +404 -404
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1757
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1705 -1681
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -258
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -79
- package/apps/shared/components/mail/reading/messageExport.ts +59 -59
- package/apps/shared/components/mail/reading/printMessage.ts +99 -99
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -443
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderOfType.ts +49 -49
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/sendJob.ts +445 -445
- package/apps/shared/mail/outbox/sendOutcomes.ts +155 -155
- package/apps/shared/mail/reportNotices.ts +66 -66
- package/apps/shared/mail/senderBlocking.ts +141 -141
- package/apps/shared/mail/useMailConnection.ts +205 -205
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -60
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -396
- package/apps/www/calendar/index.tsx +581 -581
- package/apps/www/contacts/[uid].tsx +112 -112
- package/apps/www/index.tsx +3012 -3012
- package/apps/www/messages/[uid].tsx +139 -139
- package/apps/www/settings/auto-reply/index.tsx +136 -136
- package/apps/www/settings/blocked-senders/index.tsx +303 -303
- package/apps/www/settings/encryption/index.tsx +1290 -1290
- package/apps/www/settings/filters/[uid].tsx +179 -179
- package/apps/www/settings/filters/index.tsx +105 -105
- package/apps/www/settings/filters/new/index.tsx +165 -165
- package/apps/www/settings/labels/index.tsx +207 -207
- package/apps/www/settings/privacy/index.tsx +495 -495
- package/apps/www/settings/profile/index.tsx +251 -251
- package/apps/www/settings/read-receipts/index.tsx +150 -150
- package/apps/www/settings/sharing/index.tsx +259 -259
- package/apps/www/settings/signatures/[uid].tsx +175 -175
- package/apps/www/settings/signatures/index.tsx +91 -91
- package/apps/www/settings/signatures/new/index.tsx +138 -138
- package/apps/www/tasks/index.tsx +654 -654
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +2 -2
- package/dist/apps/shared/components/layout/AppShell.js +4 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +7 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +20 -1
- package/dist/apps/shared/components/mail/reading/printMessage.js +11 -11
- package/dist/apps/shared/styles/app.css +396 -396
- package/package.json +2 -2
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { useRef, useState } from "react";
|
|
6
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
-
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
8
|
-
|
|
9
|
-
/** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
|
|
10
|
-
export const REQUEST_LIST_PAGE_SIZE = 50;
|
|
11
|
-
|
|
12
|
-
export interface PagedList<T> {
|
|
13
|
-
items: T[];
|
|
14
|
-
/** `true` until the first page has loaded (or failed). */
|
|
15
|
-
loading: boolean;
|
|
16
|
-
loadError: string | null;
|
|
17
|
-
/** Whether the last page fetched was full, so another may follow. */
|
|
18
|
-
hasMore: boolean;
|
|
19
|
-
loadingMore: boolean;
|
|
20
|
-
/** Re-fetches from the first page, dropping anything loaded beyond it. */
|
|
21
|
-
reload: () => Promise<void>;
|
|
22
|
-
/** Fetches the next page and appends what's new in it. */
|
|
23
|
-
loadMore: () => Promise<void>;
|
|
24
|
-
/** Replaces one already-loaded item in place (e.g. with an action's response). */
|
|
25
|
-
replaceItem: (item: T) => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A newest-first list fetched a page at a time with "Load more". Only the most recently started `reload()`'s
|
|
30
|
-
* responses are applied, so a slow earlier fetch (e.g. the mount fetch racing a reload after a create) can't
|
|
31
|
-
* overwrite a newer list, and a "Load more" that was in flight during a reload is dropped.
|
|
32
|
-
*
|
|
33
|
-
* Paging is by offset, so "Load more" re-reads the previous page alongside the next one and continues after the last
|
|
34
|
-
* row already shown: rows created since shift older rows onto later pages (repeats are skipped), and rows deleted
|
|
35
|
-
* since shift them onto earlier pages (which a plain next-page fetch would silently skip). If that row can't be found
|
|
36
|
-
* in the two pages at all (it was deleted, or more than a page of rows came or went), the list reloads from the start.
|
|
37
|
-
*/
|
|
38
|
-
export function usePagedList<T extends { uid: string }>(
|
|
39
|
-
fetchPage: (params: { limit: number; page: number }) => Promise<T[]>,
|
|
40
|
-
errorMessage: string,
|
|
41
|
-
): PagedList<T> {
|
|
42
|
-
const pageSize = REQUEST_LIST_PAGE_SIZE;
|
|
43
|
-
const [items, setItems] = useState<T[]>([]);
|
|
44
|
-
const [loading, setLoading] = useState(true);
|
|
45
|
-
const [loadError, setLoadError] = useState<string | null>(null);
|
|
46
|
-
const [hasMore, setHasMore] = useState(false);
|
|
47
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
48
|
-
const generation = useRef(0);
|
|
49
|
-
const nextPage = useRef(0);
|
|
50
|
-
const loaded = useRef<T[]>(items);
|
|
51
|
-
loaded.current = items;
|
|
52
|
-
|
|
53
|
-
function describe(err: unknown): string {
|
|
54
|
-
return err instanceof ApiRequestError ? err.message : errorMessage;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function reload() {
|
|
58
|
-
const current = ++generation.current;
|
|
59
|
-
try {
|
|
60
|
-
const data = await fetchPage({ limit: pageSize, page: 0 });
|
|
61
|
-
if (current !== generation.current) return;
|
|
62
|
-
setItems(data);
|
|
63
|
-
setHasMore(data.length === pageSize);
|
|
64
|
-
setLoadError(null);
|
|
65
|
-
nextPage.current = 1;
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (current === generation.current) setLoadError(describe(err));
|
|
68
|
-
} finally {
|
|
69
|
-
if (current === generation.current) setLoading(false);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function loadMore() {
|
|
74
|
-
const current = generation.current;
|
|
75
|
-
const page = nextPage.current;
|
|
76
|
-
setLoadingMore(true);
|
|
77
|
-
try {
|
|
78
|
-
const [previous, data] = await Promise.all([
|
|
79
|
-
fetchPage({ limit: pageSize, page: page - 1 }),
|
|
80
|
-
fetchPage({ limit: pageSize, page }),
|
|
81
|
-
]);
|
|
82
|
-
if (current !== generation.current) return;
|
|
83
|
-
const window: T[] = [...previous, ...data];
|
|
84
|
-
// "Load more" is only offered after a full page, so at least one row is shown.
|
|
85
|
-
const lastShown: string = loaded.current[loaded.current.length - 1].uid;
|
|
86
|
-
const boundary: number = window.map((item) => item.uid).lastIndexOf(lastShown);
|
|
87
|
-
if (boundary === -1) {
|
|
88
|
-
await reload();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const after: T[] = window.slice(boundary + 1);
|
|
92
|
-
setItems((prev) => [...prev, ...after.filter((item) => !prev.some((existing) => existing.uid === item.uid))]);
|
|
93
|
-
setHasMore(data.length === pageSize);
|
|
94
|
-
setLoadError(null);
|
|
95
|
-
nextPage.current = page + 1;
|
|
96
|
-
} catch (err) {
|
|
97
|
-
if (current === generation.current) setLoadError(describe(err));
|
|
98
|
-
} finally {
|
|
99
|
-
setLoadingMore(false);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function replaceItem(item: T) {
|
|
104
|
-
setItems((prev) => prev.map((existing) => (existing.uid === item.uid ? item : existing)));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { items, loading, loadError, hasMore, loadingMore, reload, loadMore, replaceItem };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The "Load more" button for a `usePagedList()` list - renders nothing once there's nothing more to load. */
|
|
111
|
-
export function LoadMoreButton({ list, label }: { list: Pick<PagedList<unknown>, "hasMore" | "loadingMore" | "loadMore">; label: string }) {
|
|
112
|
-
if (!list.hasMore) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
return (
|
|
116
|
-
<div className="mt-3">
|
|
117
|
-
<Button
|
|
118
|
-
type="button"
|
|
119
|
-
variant="secondary"
|
|
120
|
-
className="!w-auto"
|
|
121
|
-
loading={list.loadingMore}
|
|
122
|
-
disabled={list.loadingMore}
|
|
123
|
-
onClick={() => void list.loadMore()}
|
|
124
|
-
>
|
|
125
|
-
{label}
|
|
126
|
-
</Button>
|
|
127
|
-
</div>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useRef, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
8
|
+
|
|
9
|
+
/** Page size for the async-request lists (the server returns them newest first and caps `limit` at 500). */
|
|
10
|
+
export const REQUEST_LIST_PAGE_SIZE = 50;
|
|
11
|
+
|
|
12
|
+
export interface PagedList<T> {
|
|
13
|
+
items: T[];
|
|
14
|
+
/** `true` until the first page has loaded (or failed). */
|
|
15
|
+
loading: boolean;
|
|
16
|
+
loadError: string | null;
|
|
17
|
+
/** Whether the last page fetched was full, so another may follow. */
|
|
18
|
+
hasMore: boolean;
|
|
19
|
+
loadingMore: boolean;
|
|
20
|
+
/** Re-fetches from the first page, dropping anything loaded beyond it. */
|
|
21
|
+
reload: () => Promise<void>;
|
|
22
|
+
/** Fetches the next page and appends what's new in it. */
|
|
23
|
+
loadMore: () => Promise<void>;
|
|
24
|
+
/** Replaces one already-loaded item in place (e.g. with an action's response). */
|
|
25
|
+
replaceItem: (item: T) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A newest-first list fetched a page at a time with "Load more". Only the most recently started `reload()`'s
|
|
30
|
+
* responses are applied, so a slow earlier fetch (e.g. the mount fetch racing a reload after a create) can't
|
|
31
|
+
* overwrite a newer list, and a "Load more" that was in flight during a reload is dropped.
|
|
32
|
+
*
|
|
33
|
+
* Paging is by offset, so "Load more" re-reads the previous page alongside the next one and continues after the last
|
|
34
|
+
* row already shown: rows created since shift older rows onto later pages (repeats are skipped), and rows deleted
|
|
35
|
+
* since shift them onto earlier pages (which a plain next-page fetch would silently skip). If that row can't be found
|
|
36
|
+
* in the two pages at all (it was deleted, or more than a page of rows came or went), the list reloads from the start.
|
|
37
|
+
*/
|
|
38
|
+
export function usePagedList<T extends { uid: string }>(
|
|
39
|
+
fetchPage: (params: { limit: number; page: number }) => Promise<T[]>,
|
|
40
|
+
errorMessage: string,
|
|
41
|
+
): PagedList<T> {
|
|
42
|
+
const pageSize = REQUEST_LIST_PAGE_SIZE;
|
|
43
|
+
const [items, setItems] = useState<T[]>([]);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
46
|
+
const [hasMore, setHasMore] = useState(false);
|
|
47
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
48
|
+
const generation = useRef(0);
|
|
49
|
+
const nextPage = useRef(0);
|
|
50
|
+
const loaded = useRef<T[]>(items);
|
|
51
|
+
loaded.current = items;
|
|
52
|
+
|
|
53
|
+
function describe(err: unknown): string {
|
|
54
|
+
return err instanceof ApiRequestError ? err.message : errorMessage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function reload() {
|
|
58
|
+
const current = ++generation.current;
|
|
59
|
+
try {
|
|
60
|
+
const data = await fetchPage({ limit: pageSize, page: 0 });
|
|
61
|
+
if (current !== generation.current) return;
|
|
62
|
+
setItems(data);
|
|
63
|
+
setHasMore(data.length === pageSize);
|
|
64
|
+
setLoadError(null);
|
|
65
|
+
nextPage.current = 1;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (current === generation.current) setLoadError(describe(err));
|
|
68
|
+
} finally {
|
|
69
|
+
if (current === generation.current) setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function loadMore() {
|
|
74
|
+
const current = generation.current;
|
|
75
|
+
const page = nextPage.current;
|
|
76
|
+
setLoadingMore(true);
|
|
77
|
+
try {
|
|
78
|
+
const [previous, data] = await Promise.all([
|
|
79
|
+
fetchPage({ limit: pageSize, page: page - 1 }),
|
|
80
|
+
fetchPage({ limit: pageSize, page }),
|
|
81
|
+
]);
|
|
82
|
+
if (current !== generation.current) return;
|
|
83
|
+
const window: T[] = [...previous, ...data];
|
|
84
|
+
// "Load more" is only offered after a full page, so at least one row is shown.
|
|
85
|
+
const lastShown: string = loaded.current[loaded.current.length - 1].uid;
|
|
86
|
+
const boundary: number = window.map((item) => item.uid).lastIndexOf(lastShown);
|
|
87
|
+
if (boundary === -1) {
|
|
88
|
+
await reload();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const after: T[] = window.slice(boundary + 1);
|
|
92
|
+
setItems((prev) => [...prev, ...after.filter((item) => !prev.some((existing) => existing.uid === item.uid))]);
|
|
93
|
+
setHasMore(data.length === pageSize);
|
|
94
|
+
setLoadError(null);
|
|
95
|
+
nextPage.current = page + 1;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (current === generation.current) setLoadError(describe(err));
|
|
98
|
+
} finally {
|
|
99
|
+
setLoadingMore(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function replaceItem(item: T) {
|
|
104
|
+
setItems((prev) => prev.map((existing) => (existing.uid === item.uid ? item : existing)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { items, loading, loadError, hasMore, loadingMore, reload, loadMore, replaceItem };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The "Load more" button for a `usePagedList()` list - renders nothing once there's nothing more to load. */
|
|
111
|
+
export function LoadMoreButton({ list, label }: { list: Pick<PagedList<unknown>, "hasMore" | "loadingMore" | "loadMore">; label: string }) {
|
|
112
|
+
if (!list.hasMore) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<div className="mt-3">
|
|
117
|
+
<Button
|
|
118
|
+
type="button"
|
|
119
|
+
variant="secondary"
|
|
120
|
+
className="!w-auto"
|
|
121
|
+
loading={list.loadingMore}
|
|
122
|
+
disabled={list.loadingMore}
|
|
123
|
+
onClick={() => void list.loadMore()}
|
|
124
|
+
>
|
|
125
|
+
{label}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|