@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.
- package/apps/admin/audit-log/index.tsx +35 -8
- package/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +118 -82
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +717 -513
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -314
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +270 -169
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
- package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +316 -0
- package/apps/shared/search/localIndexSchema.ts +236 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +266 -0
- package/apps/shared/search/localIndexWorker.ts +926 -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 +438 -407
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +148 -17
- package/apps/www/index.tsx +1314 -486
- package/apps/www/messages/[uid].tsx +23 -7
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/booking-types/[uid].tsx +306 -300
- package/apps/www/settings/encryption/index.tsx +800 -215
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +274 -0
- package/apps/www/tasks/index.tsx +638 -530
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.js +17 -1
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +154 -21
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/layout/AppShell.js +100 -8
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +408 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
- package/dist/apps/shared/search/localIndexSchema.js +185 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.js +258 -0
- package/dist/apps/shared/search/localIndexWorker.js +687 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +38 -18
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.js +94 -15
- package/dist/apps/www/index.js +770 -137
- package/dist/apps/www/messages/[uid].js +23 -7
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/booking-types/[uid].js +17 -12
- package/dist/apps/www/settings/encryption/index.js +541 -96
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.js +124 -0
- package/dist/apps/www/tasks/index.js +76 -14
- package/package.json +3 -2
|
@@ -9,6 +9,7 @@ import ContactsShell, { ContactsShellProps } from "../../shared/components/conta
|
|
|
9
9
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
10
10
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
|
+
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
@@ -30,6 +31,9 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
30
31
|
const [contact, setContact] = useState<Contact | null>(null);
|
|
31
32
|
const [loading, setLoading] = useState(true);
|
|
32
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
// Kept apart from the load `error` - a failed delete must leave the contact on screen (with the
|
|
35
|
+
// reason) rather than replacing the whole page with an error as if it had never loaded.
|
|
36
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
33
37
|
const [mode, setMode] = useState<Mode>("view");
|
|
34
38
|
|
|
35
39
|
useEffect(() => {
|
|
@@ -46,14 +50,22 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
46
50
|
// rendered once `contact` is already resolved (see the `error || !contact` guard above it), so this
|
|
47
51
|
// never runs with a stale/absent contact — no redundant null check needed here.
|
|
48
52
|
async function handleDelete(contact: Contact) {
|
|
53
|
+
setDeleteError(null);
|
|
49
54
|
try {
|
|
50
55
|
await deleteContact(contact.uid, contact.version);
|
|
56
|
+
// A deleted contact's pinned signing keys must stop vouching for signatures.
|
|
57
|
+
clearPinnedSignerCache();
|
|
51
58
|
window.location.href = "/contacts";
|
|
52
59
|
} catch (err) {
|
|
53
|
-
|
|
60
|
+
setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this contact.");
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
// After a key change was resolved (or found stale): re-read the contact, keeping the one shown if that fails.
|
|
65
|
+
function handleKeysChanged() {
|
|
66
|
+
getContact(uid).then(setContact, () => undefined);
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
if (loading) {
|
|
58
70
|
return <p className="p-8 text-sm text-text-muted">Loading…</p>;
|
|
59
71
|
}
|
|
@@ -82,7 +94,14 @@ function ContactDetailContent({ uid }: { uid: string }) {
|
|
|
82
94
|
|
|
83
95
|
return (
|
|
84
96
|
<div className="p-6">
|
|
85
|
-
|
|
97
|
+
{deleteError && <Alert>{deleteError}</Alert>}
|
|
98
|
+
<ContactDetailPane
|
|
99
|
+
contact={contact}
|
|
100
|
+
onEdit={() => setMode("edit")}
|
|
101
|
+
onDelete={() => handleDelete(contact)}
|
|
102
|
+
backHref={backHref}
|
|
103
|
+
onKeysChanged={handleKeysChanged}
|
|
104
|
+
/>
|
|
86
105
|
</div>
|
|
87
106
|
);
|
|
88
107
|
}
|
|
@@ -22,7 +22,13 @@ import ContactsToolbar from "../../shared/components/contacts/ContactsToolbar.js
|
|
|
22
22
|
import ContactAvatar from "@rapidmx/react-shared/components/avatar/ContactAvatar.js";
|
|
23
23
|
import ContactDetailPane from "../../shared/components/contacts/ContactDetailPane.js";
|
|
24
24
|
import ContactForm from "../../shared/components/contacts/ContactForm.js";
|
|
25
|
+
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
26
|
+
import { getMyMailboxAccess } from "@rapidmx/react-shared/mail/mailboxAccessApi.js";
|
|
25
27
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
28
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
29
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
30
|
+
import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
|
|
31
|
+
import { clearPinnedSignerCache } from "../../shared/components/mail/pinnedSigners.js";
|
|
26
32
|
|
|
27
33
|
const INPUT_CLASS =
|
|
28
34
|
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -30,7 +36,7 @@ const INPUT_CLASS =
|
|
|
30
36
|
export default function ContactsPage(props: ContactsShellProps) {
|
|
31
37
|
return (
|
|
32
38
|
<ContactsShell {...props}>
|
|
33
|
-
<ContactsContent />
|
|
39
|
+
<ContactsContent userUid={props.userUid} />
|
|
34
40
|
</ContactsShell>
|
|
35
41
|
);
|
|
36
42
|
}
|
|
@@ -38,22 +44,33 @@ export default function ContactsPage(props: ContactsShellProps) {
|
|
|
38
44
|
type Mode = "view" | "edit" | "new";
|
|
39
45
|
type SortColumn = "name" | "info";
|
|
40
46
|
|
|
47
|
+
/** Shown in the Deleted view when the server answered `?deleted=true` with live contacts (see `canViewDeleted`). */
|
|
48
|
+
const DELETED_NOT_PERMITTED_MESSAGE = "You don't have permission to view deleted contacts in this folder.";
|
|
49
|
+
|
|
41
50
|
function primaryInfo(contact: Contact): string {
|
|
42
51
|
return contact.emails[0]?.address ?? contact.phones[0]?.phoneNumber ?? "";
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
/** How long an export's object URL is kept alive after the click - some browsers are still reading the
|
|
55
|
+
* blob after `click()` returns, so revoking it synchronously can cancel the download. */
|
|
56
|
+
const VCARD_DOWNLOAD_URL_LIFETIME_MS = 60_000;
|
|
57
|
+
|
|
45
58
|
function downloadTextFile(filename: string, content: string): void {
|
|
46
59
|
const blob = new Blob([content], { type: "text/vcard" });
|
|
47
60
|
const url = URL.createObjectURL(blob);
|
|
48
61
|
const a = document.createElement("a");
|
|
49
62
|
a.href = url;
|
|
50
63
|
a.download = filename;
|
|
64
|
+
document.body.appendChild(a);
|
|
51
65
|
a.click();
|
|
52
|
-
|
|
66
|
+
a.remove();
|
|
67
|
+
setTimeout(() => URL.revokeObjectURL(url), VCARD_DOWNLOAD_URL_LIFETIME_MS);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
function ContactsContent() {
|
|
56
|
-
const { folderUid, mailboxUid } = useContactsShell();
|
|
70
|
+
function ContactsContent({ userUid }: { userUid?: string }) {
|
|
71
|
+
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
72
|
+
// The new-contact form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
|
|
73
|
+
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
|
|
57
74
|
const { openCompose } = useCompose();
|
|
58
75
|
const isMobile = useIsMobile();
|
|
59
76
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
@@ -62,6 +79,9 @@ function ContactsContent() {
|
|
|
62
79
|
const [deletedError, setDeletedError] = useState<string | null>(null);
|
|
63
80
|
const [loading, setLoading] = useState(true);
|
|
64
81
|
const [error, setError] = useState<string | null>(null);
|
|
82
|
+
// Set when `listAllPages()` stopped at its page cap - the list shown isn't every contact.
|
|
83
|
+
const [truncated, setTruncated] = useState(false);
|
|
84
|
+
const [deletedTruncated, setDeletedTruncated] = useState(false);
|
|
65
85
|
const [query, setQuery] = useState("");
|
|
66
86
|
const [view, setView] = useState<ContactsView>({ type: "all" });
|
|
67
87
|
const [sortColumn, setSortColumn] = useState<SortColumn>("name");
|
|
@@ -69,6 +89,9 @@ function ContactsContent() {
|
|
|
69
89
|
const [checkedUids, setCheckedUids] = useState<Set<string>>(new Set());
|
|
70
90
|
const [selectedUid, setSelectedUid] = useState<string | null>(null);
|
|
71
91
|
const [mode, setMode] = useState<Mode>("view");
|
|
92
|
+
// Set after a contact is created in a different mailbox than the one this list shows.
|
|
93
|
+
const [savedElsewhere, setSavedElsewhere] = useState<{ mailboxUid: string; displayName: string } | null>(null);
|
|
94
|
+
const [confirmingBulkDelete, setConfirmingBulkDelete] = useState(false);
|
|
72
95
|
|
|
73
96
|
/** Returns a promise so bulk actions (below) can wait for the refreshed list before re-asserting their
|
|
74
97
|
* own error message — this always clears `error` first (a legitimate reset for a fresh fetch attempt),
|
|
@@ -77,13 +100,17 @@ function ContactsContent() {
|
|
|
77
100
|
function reload(): Promise<void> {
|
|
78
101
|
if (!folderUid) {
|
|
79
102
|
setContacts([]);
|
|
103
|
+
setTruncated(false);
|
|
80
104
|
setLoading(false);
|
|
81
105
|
return Promise.resolve();
|
|
82
106
|
}
|
|
83
107
|
setLoading(true);
|
|
84
108
|
setError(null);
|
|
85
|
-
return listContacts(folderUid, { limit:
|
|
86
|
-
.then(
|
|
109
|
+
return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
|
|
110
|
+
.then((result) => {
|
|
111
|
+
setContacts(result.items);
|
|
112
|
+
setTruncated(result.truncated);
|
|
113
|
+
})
|
|
87
114
|
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load contacts."))
|
|
88
115
|
.finally(() => setLoading(false));
|
|
89
116
|
}
|
|
@@ -92,14 +119,56 @@ function ContactsContent() {
|
|
|
92
119
|
void reload();
|
|
93
120
|
}, [folderUid]);
|
|
94
121
|
|
|
122
|
+
// restapi only honors `?deleted=true` for a caller with both delete and update rights on the contacts *folder*
|
|
123
|
+
// (its ACL, which inherits the mailbox's but can carry its own records) - anyone else silently gets the *live*
|
|
124
|
+
// contacts back, which the Deleted view would then present as deleted. There is no client API for the caller's
|
|
125
|
+
// access to a folder, so the mailbox-level check below is only a first gate (an owner always passes; anyone else
|
|
126
|
+
// asks the server; unknown - still loading, or the check failed - hides the view), and the Deleted view's own
|
|
127
|
+
// load below detects the server ignoring the filter. `ContactsShell` only renders this with a resolved mailbox,
|
|
128
|
+
// and switching mailboxes is a full page load, so `mailboxUid` never changes under this component.
|
|
129
|
+
const ownsMailbox = mailboxes.some((mb) => mb.uid === mailboxUid && mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
|
|
130
|
+
const [delegateCanViewDeleted, setDelegateCanViewDeleted] = useState(false);
|
|
131
|
+
// Resolving a contact's key change needs update rights on the mailbox - `undefined` while unknown.
|
|
132
|
+
const [delegateCanUpdate, setDelegateCanUpdate] = useState<boolean | undefined>(undefined);
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (ownsMailbox) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let cancelled = false;
|
|
138
|
+
getMyMailboxAccess(mailboxUid!).then(
|
|
139
|
+
(access) => {
|
|
140
|
+
if (!cancelled) {
|
|
141
|
+
setDelegateCanViewDeleted(access.canDelete && access.canUpdate);
|
|
142
|
+
setDelegateCanUpdate(access.canUpdate);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
() => undefined,
|
|
146
|
+
);
|
|
147
|
+
return () => {
|
|
148
|
+
cancelled = true;
|
|
149
|
+
};
|
|
150
|
+
}, [ownsMailbox]);
|
|
151
|
+
const canViewDeleted = ownsMailbox || delegateCanViewDeleted;
|
|
152
|
+
|
|
95
153
|
useEffect(() => {
|
|
96
154
|
if (view.type !== "deleted" || !folderUid) {
|
|
97
155
|
return;
|
|
98
156
|
}
|
|
99
157
|
setDeletedLoading(true);
|
|
100
158
|
setDeletedError(null);
|
|
101
|
-
listDeletedContacts(folderUid, { limit:
|
|
102
|
-
.then(
|
|
159
|
+
listAllPages((page) => listDeletedContacts(folderUid, { limit: LIST_PAGE_SIZE, page }))
|
|
160
|
+
.then((result) => {
|
|
161
|
+
// A server that dropped the `deleted` filter (no delete+update right on this folder) answers with live
|
|
162
|
+
// contacts, which must never be listed as deleted.
|
|
163
|
+
if (result.items.some((c) => c.deleted !== true)) {
|
|
164
|
+
setDeletedContacts([]);
|
|
165
|
+
setDeletedTruncated(false);
|
|
166
|
+
setDeletedError(DELETED_NOT_PERMITTED_MESSAGE);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setDeletedContacts(result.items);
|
|
170
|
+
setDeletedTruncated(result.truncated);
|
|
171
|
+
})
|
|
103
172
|
.catch((err) => setDeletedError(err instanceof ApiRequestError ? err.message : "Could not load deleted contacts."))
|
|
104
173
|
.finally(() => setDeletedLoading(false));
|
|
105
174
|
// Re-fetches every time the Deleted view is (re-)selected, matching this page's own `reload()`
|
|
@@ -192,8 +261,15 @@ function ContactsContent() {
|
|
|
192
261
|
}
|
|
193
262
|
|
|
194
263
|
function handleSaved(contact: Contact) {
|
|
195
|
-
setSelectedUid(contact.uid);
|
|
196
264
|
setMode("view");
|
|
265
|
+
if (contact.mailboxUid !== mailboxUid) {
|
|
266
|
+
// Created in another mailbox (via the form's Mailbox selector) - it won't be in this list.
|
|
267
|
+
setSelectedUid(null);
|
|
268
|
+
setSavedElsewhere({ mailboxUid: contact.mailboxUid, displayName: contact.displayName });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setSavedElsewhere(null);
|
|
272
|
+
setSelectedUid(contact.uid);
|
|
197
273
|
void reload();
|
|
198
274
|
}
|
|
199
275
|
|
|
@@ -204,6 +280,8 @@ function ContactsContent() {
|
|
|
204
280
|
async function handleDelete(contact: Contact) {
|
|
205
281
|
try {
|
|
206
282
|
await deleteContact(contact.uid, contact.version);
|
|
283
|
+
// Trusted signer pins come from contacts - a deleted contact's keys must stop vouching for signatures.
|
|
284
|
+
clearPinnedSignerCache();
|
|
207
285
|
setSelectedUid(null);
|
|
208
286
|
setMode("view");
|
|
209
287
|
void reload();
|
|
@@ -213,6 +291,7 @@ function ContactsContent() {
|
|
|
213
291
|
}
|
|
214
292
|
|
|
215
293
|
async function handleBulkDelete() {
|
|
294
|
+
setConfirmingBulkDelete(false);
|
|
216
295
|
let bulkError: string | null = null;
|
|
217
296
|
for (const contact of checkedContacts) {
|
|
218
297
|
try {
|
|
@@ -221,6 +300,7 @@ function ContactsContent() {
|
|
|
221
300
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not delete one or more contacts.";
|
|
222
301
|
}
|
|
223
302
|
}
|
|
303
|
+
clearPinnedSignerCache();
|
|
224
304
|
setCheckedUids(new Set());
|
|
225
305
|
await reload();
|
|
226
306
|
setError(bulkError);
|
|
@@ -237,9 +317,9 @@ function ContactsContent() {
|
|
|
237
317
|
|
|
238
318
|
function handleEmail() {
|
|
239
319
|
const addresses = checkedContacts.map((c) => c.emails[0]?.address).filter((a): a is string => Boolean(a));
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
openCompose({
|
|
320
|
+
// No mailboxUid: a fresh message defaults to the caller's own mailbox, not whichever mailbox's
|
|
321
|
+
// contacts happen to be open - the compose window's From field can still change it.
|
|
322
|
+
openCompose({ to: addresses.join(", ") });
|
|
243
323
|
}
|
|
244
324
|
|
|
245
325
|
async function handleToggleFavorite() {
|
|
@@ -252,6 +332,7 @@ function ContactsContent() {
|
|
|
252
332
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
|
|
253
333
|
}
|
|
254
334
|
}
|
|
335
|
+
clearPinnedSignerCache();
|
|
255
336
|
await reload();
|
|
256
337
|
setError(bulkError);
|
|
257
338
|
}
|
|
@@ -265,11 +346,14 @@ function ContactsContent() {
|
|
|
265
346
|
for (const contact of checkedContacts) {
|
|
266
347
|
const categories = Array.from(new Set([...(contact.categories ?? []), category.trim()]));
|
|
267
348
|
try {
|
|
268
|
-
|
|
349
|
+
// Only the changed field - sending the whole fetched contact back includes server-managed
|
|
350
|
+
// fields restapi rejects, and would overwrite any concurrent edit to the other fields.
|
|
351
|
+
await updateContact({ uid: contact.uid, version: contact.version, categories });
|
|
269
352
|
} catch (err) {
|
|
270
353
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more contacts.";
|
|
271
354
|
}
|
|
272
355
|
}
|
|
356
|
+
clearPinnedSignerCache();
|
|
273
357
|
await reload();
|
|
274
358
|
setError(bulkError);
|
|
275
359
|
}
|
|
@@ -295,13 +379,14 @@ function ContactsContent() {
|
|
|
295
379
|
bulkError = err instanceof ApiRequestError ? err.message : "Could not import one or more contacts.";
|
|
296
380
|
}
|
|
297
381
|
}
|
|
382
|
+
clearPinnedSignerCache();
|
|
298
383
|
await reload();
|
|
299
384
|
setError(bulkError);
|
|
300
385
|
}
|
|
301
386
|
|
|
302
387
|
return (
|
|
303
388
|
<div className="flex-1 flex min-h-0">
|
|
304
|
-
<ContactsSidebar mailboxUid={mailboxUid} contacts={contacts} active={view} onSelect={handleSelectView} />
|
|
389
|
+
<ContactsSidebar mailboxUid={mailboxUid} contacts={contacts} active={view} onSelect={handleSelectView} showDeleted={canViewDeleted} />
|
|
305
390
|
{/* Hidden on mobile while the "new contact" form (the one form-pane state mobile keeps
|
|
306
391
|
in-place — see the pane's own comment below) is showing, so the two never compete for the
|
|
307
392
|
same row's width. Desktop always shows both side by side, unchanged. */}
|
|
@@ -316,7 +401,7 @@ function ContactsContent() {
|
|
|
316
401
|
allSelectedFavorited={checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite)}
|
|
317
402
|
onNewContact={handleNew}
|
|
318
403
|
onEdit={handleToolbarEdit}
|
|
319
|
-
onDelete={
|
|
404
|
+
onDelete={() => setConfirmingBulkDelete(true)}
|
|
320
405
|
onEmail={handleEmail}
|
|
321
406
|
onToggleFavorite={handleToggleFavorite}
|
|
322
407
|
onAddCategory={handleAddCategory}
|
|
@@ -338,6 +423,14 @@ function ContactsContent() {
|
|
|
338
423
|
<Alert>{error}</Alert>
|
|
339
424
|
</div>
|
|
340
425
|
)}
|
|
426
|
+
{(isDeletedView ? deletedTruncated : truncated) && (
|
|
427
|
+
<div className="p-3">
|
|
428
|
+
<Alert>
|
|
429
|
+
This folder has more contacts than can be shown at once - only the first {LIST_PAGE_SIZE * MAX_LIST_PAGES} are
|
|
430
|
+
listed.
|
|
431
|
+
</Alert>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
341
434
|
{deletedError && isDeletedView && (
|
|
342
435
|
<div className="p-3">
|
|
343
436
|
<Alert>{deletedError}</Alert>
|
|
@@ -421,16 +514,54 @@ function ContactsContent() {
|
|
|
421
514
|
see handleSelectRow), so this pane only needs to show there for "new", which stays in-place
|
|
422
515
|
on every device (an unsaved contact has no uid for a route). Always visible on desktop. */}
|
|
423
516
|
<div className={["flex-1 min-w-0 overflow-y-auto p-6 md:block", mode === "new" ? "block" : "hidden"].join(" ")}>
|
|
517
|
+
{savedElsewhere && mode !== "new" && (
|
|
518
|
+
<p role="status" className="mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text">
|
|
519
|
+
{savedElsewhere.displayName} was added to{" "}
|
|
520
|
+
{mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox"}.{" "}
|
|
521
|
+
<a
|
|
522
|
+
href={`/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`}
|
|
523
|
+
className="font-medium text-primary-dark hover:underline"
|
|
524
|
+
>
|
|
525
|
+
View that mailbox’s contacts
|
|
526
|
+
</a>
|
|
527
|
+
</p>
|
|
528
|
+
)}
|
|
424
529
|
{mode === "new" && mailboxUid && folderUid ? (
|
|
425
|
-
<ContactForm
|
|
530
|
+
<ContactForm
|
|
531
|
+
mailboxUid={mailboxUid}
|
|
532
|
+
folderUid={folderUid}
|
|
533
|
+
mailboxes={writableMailboxes}
|
|
534
|
+
onSaved={handleSaved}
|
|
535
|
+
onCancel={handleCancel}
|
|
536
|
+
/>
|
|
426
537
|
) : mode === "edit" && selected ? (
|
|
427
538
|
<ContactForm contact={selected} onSaved={handleSaved} onCancel={handleCancel} />
|
|
428
539
|
) : selected ? (
|
|
429
|
-
<ContactDetailPane
|
|
540
|
+
<ContactDetailPane
|
|
541
|
+
contact={selected}
|
|
542
|
+
onEdit={() => setMode("edit")}
|
|
543
|
+
onDelete={() => handleDelete(selected)}
|
|
544
|
+
onKeysChanged={() => void reload()}
|
|
545
|
+
canResolveKeys={ownsMailbox || delegateCanUpdate}
|
|
546
|
+
/>
|
|
430
547
|
) : (
|
|
431
548
|
<p className="text-sm text-text-muted">Select a contact, or create a new one.</p>
|
|
432
549
|
)}
|
|
433
550
|
</div>
|
|
551
|
+
<Modal open={confirmingBulkDelete} onClose={() => setConfirmingBulkDelete(false)} title="Delete contacts">
|
|
552
|
+
<p className="text-sm mb-5">
|
|
553
|
+
Delete {checkedContacts.length} selected {checkedContacts.length === 1 ? "contact" : "contacts"}? They
|
|
554
|
+
move to Deleted contacts.
|
|
555
|
+
</p>
|
|
556
|
+
<div className="flex gap-3 justify-end">
|
|
557
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingBulkDelete(false)}>
|
|
558
|
+
Cancel
|
|
559
|
+
</Button>
|
|
560
|
+
<Button type="button" className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger" onClick={handleBulkDelete}>
|
|
561
|
+
Delete
|
|
562
|
+
</Button>
|
|
563
|
+
</div>
|
|
564
|
+
</Modal>
|
|
434
565
|
</div>
|
|
435
566
|
);
|
|
436
567
|
}
|