@rapidmx/web-client 0.35.0 → 0.36.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/escrow/matters/[uid].tsx +4 -3
- package/apps/shared/calendar/sealedEvents.ts +80 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +61 -3
- package/apps/shared/components/contacts/contactCardData.ts +2 -1
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +23 -1
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +91 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
- package/apps/shared/components/mail/ConversationList.tsx +1 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +35 -13
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
- package/apps/shared/components/mail/invite/inviteStore.ts +7 -1
- package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
- package/apps/shared/components/settings/KeyExportSetting.tsx +170 -0
- package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +117 -0
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +2 -2
- package/apps/shared/mail/reportNotices.ts +3 -1
- package/apps/shared/mail/zeroAccessSource.ts +27 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +31 -0
- package/apps/shared/search/localIndexBuilder.ts +27 -1
- package/apps/shared/search/localIndexRpcClient.ts +8 -4
- package/apps/shared/search/localIndexSchema.ts +2 -2
- package/apps/shared/search/localIndexTransport.ts +2 -1
- package/apps/shared/search/localIndexWorker.ts +6 -4
- package/apps/shared/search/zeroAccessIndexSync.ts +487 -0
- package/apps/www/calendar/index.tsx +176 -56
- package/apps/www/index.tsx +10 -3
- package/apps/www/settings/encryption/index.tsx +5 -0
- package/apps/www/settings/privacy/index.tsx +2 -1
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/sealedEvents.d.ts +13 -0
- package/dist/apps/shared/calendar/sealedEvents.js +54 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +18 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +2 -1
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +21 -2
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +15 -0
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +72 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +3 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +32 -0
- package/dist/apps/shared/components/mail/ConversationList.js +1 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +34 -13
- package/dist/apps/shared/components/mail/compose/draftResume.d.ts +2 -0
- package/dist/apps/shared/components/mail/compose/draftResume.js +28 -3
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/quotedBody.js +4 -3
- package/dist/apps/shared/components/mail/invite/inviteStore.js +6 -1
- package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +4 -1
- package/dist/apps/shared/components/settings/KeyExportSetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/KeyExportSetting.js +88 -0
- package/dist/apps/shared/components/settings/PolicyLockedNotice.d.ts +11 -0
- package/dist/apps/shared/components/settings/PolicyLockedNotice.js +9 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +14 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +71 -0
- package/dist/apps/shared/mail/decryptedMessages.d.ts +1 -1
- package/dist/apps/shared/mail/decryptedMessages.js +11 -3
- package/dist/apps/shared/mail/outbox/sendJob.js +2 -2
- package/dist/apps/shared/mail/reportNotices.js +3 -1
- package/dist/apps/shared/mail/zeroAccessSource.d.ts +8 -0
- package/dist/apps/shared/mail/zeroAccessSource.js +26 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +28 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +5 -0
- package/dist/apps/shared/search/localIndexBuilder.js +24 -2
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +1 -1
- package/dist/apps/shared/search/localIndexRpcClient.js +6 -4
- package/dist/apps/shared/search/localIndexSchema.d.ts +1 -1
- package/dist/apps/shared/search/localIndexSchema.js +2 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +2 -1
- package/dist/apps/shared/search/localIndexWorker.d.ts +2 -0
- package/dist/apps/shared/search/localIndexWorker.js +4 -4
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +71 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.js +382 -0
- package/dist/apps/www/calendar/index.js +41 -15
- package/dist/apps/www/index.js +10 -3
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +1 -1
- package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
- package/dist/lib/calendar/calendarApi.d.ts +6 -0
- package/dist/lib/calendar/inviteApi.js +27 -5
- package/dist/lib/calendar/sealedInvites.d.ts +11 -0
- package/dist/lib/calendar/sealedInvites.js +28 -0
- package/dist/lib/crypto/keyExport.d.ts +54 -0
- package/dist/lib/crypto/keyExport.js +161 -0
- package/dist/lib/crypto/keySession.d.ts +45 -1
- package/dist/lib/crypto/keySession.js +93 -2
- package/dist/lib/crypto/keyvaultApi.d.ts +88 -3
- package/dist/lib/crypto/keyvaultApi.js +32 -0
- package/dist/lib/crypto/messageSecurity.d.ts +30 -1
- package/dist/lib/crypto/messageSecurity.js +80 -4
- package/dist/lib/crypto/mime.d.ts +5 -0
- package/dist/lib/crypto/mime.js +25 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +50 -0
- package/dist/lib/crypto/sharedMailboxKeys.js +264 -0
- package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
- package/dist/lib/crypto/zeroAccessKey.js +66 -0
- package/dist/lib/mail/conversationsApi.d.ts +6 -0
- package/dist/lib/mail/mailApi.d.ts +30 -2
- package/dist/lib/mail/mailApi.js +5 -1
- package/dist/lib/search/zeroAccessIndexApi.d.ts +81 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/lib/admin/escrowAccessRequestsApi.ts +6 -0
- package/lib/calendar/calendarApi.ts +3 -0
- package/lib/calendar/inviteApi.ts +28 -5
- package/lib/calendar/sealedInvites.ts +33 -0
- package/lib/crypto/keyExport.ts +229 -0
- package/lib/crypto/keySession.ts +117 -3
- package/lib/crypto/keyvaultApi.ts +125 -3
- package/lib/crypto/messageSecurity.ts +110 -3
- package/lib/crypto/mime.ts +26 -0
- package/lib/crypto/sharedMailboxKeys.ts +324 -0
- package/lib/crypto/zeroAccessKey.ts +97 -0
- package/lib/mail/conversationsApi.ts +3 -0
- package/lib/mail/mailApi.ts +33 -3
- package/lib/search/zeroAccessIndexApi.ts +119 -0
- package/package.json +3 -3
|
@@ -25,15 +25,26 @@ import { ApiRequestError } from "../../../lib/util/api.js";
|
|
|
25
25
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
26
26
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
27
27
|
import { CalendarEvent, getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
28
|
+
import { openSealedEvent, openSealedEvents } from "../../shared/calendar/sealedEvents.js";
|
|
28
29
|
import { useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
29
30
|
import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
30
31
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
31
32
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
32
33
|
import { CalendarOccurrence, expandAllOccurrences, expandOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
33
34
|
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
allOccurrences,
|
|
37
|
+
initialMatchIndex,
|
|
38
|
+
occurrencesInRange,
|
|
39
|
+
searchOccurrences,
|
|
40
|
+
searchTerms,
|
|
41
|
+
stepSequenceIndex,
|
|
42
|
+
} from "../../../lib/calendar/eventSearch.js";
|
|
35
43
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
36
|
-
import CalendarShell, {
|
|
44
|
+
import CalendarShell, {
|
|
45
|
+
CalendarShellProps,
|
|
46
|
+
useCalendarShell,
|
|
47
|
+
} from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
37
48
|
import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
|
|
38
49
|
import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
|
|
39
50
|
import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
|
|
@@ -49,7 +60,13 @@ import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
|
|
|
49
60
|
import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
|
|
50
61
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
51
62
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
52
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
CALENDAR_VIEWS,
|
|
65
|
+
CalendarView,
|
|
66
|
+
getStoredCalendarView,
|
|
67
|
+
isCalendarView,
|
|
68
|
+
storeCalendarView,
|
|
69
|
+
} from "../../shared/components/calendar/calendarView.js";
|
|
53
70
|
import { SlideFrom } from "../../shared/components/calendar/SlideIn.js";
|
|
54
71
|
import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
|
|
55
72
|
import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
|
|
@@ -78,7 +95,14 @@ function CalendarPage(props: CalendarShellProps) {
|
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
type ViewType = CalendarView;
|
|
81
|
-
const VIEW_LABELS: Record<ViewType, string> = {
|
|
98
|
+
const VIEW_LABELS: Record<ViewType, string> = {
|
|
99
|
+
list: "List",
|
|
100
|
+
month: "Month",
|
|
101
|
+
week: "Week",
|
|
102
|
+
workWeek: "Work Week",
|
|
103
|
+
day: "Day",
|
|
104
|
+
split: "Split",
|
|
105
|
+
};
|
|
82
106
|
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
83
107
|
const VIEW_SHORTCUTS: Partial<Record<ViewType, ShortcutDef>> = {
|
|
84
108
|
list: SHORTCUTS.calendar.list,
|
|
@@ -89,7 +113,15 @@ const VIEW_SHORTCUTS: Partial<Record<ViewType, ShortcutDef>> = {
|
|
|
89
113
|
};
|
|
90
114
|
|
|
91
115
|
/** One of the view switcher's buttons, with its shortcut in the tooltip. */
|
|
92
|
-
function ViewButton({
|
|
116
|
+
function ViewButton({
|
|
117
|
+
view,
|
|
118
|
+
current,
|
|
119
|
+
onSelect,
|
|
120
|
+
}: {
|
|
121
|
+
view: ViewType;
|
|
122
|
+
current: ViewType;
|
|
123
|
+
onSelect: (view: ViewType) => void;
|
|
124
|
+
}) {
|
|
93
125
|
const shortcut = VIEW_SHORTCUTS[view];
|
|
94
126
|
const hint = useShortcutProps(VIEW_LABELS[view], shortcut ?? SHORTCUTS.calendar.day, !!shortcut);
|
|
95
127
|
return (
|
|
@@ -124,7 +156,8 @@ interface ModalState {
|
|
|
124
156
|
}
|
|
125
157
|
|
|
126
158
|
function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHref?: string }) {
|
|
127
|
-
const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } =
|
|
159
|
+
const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } =
|
|
160
|
+
useCalendarShell();
|
|
128
161
|
const client = useApiClient();
|
|
129
162
|
// `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
|
|
130
163
|
// scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
|
|
@@ -133,7 +166,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
133
166
|
// dnd-kit's own documented pattern for this exact conflict.
|
|
134
167
|
const sensors = useSensors(
|
|
135
168
|
useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),
|
|
136
|
-
useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })
|
|
169
|
+
useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })
|
|
137
170
|
);
|
|
138
171
|
|
|
139
172
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
@@ -194,7 +227,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
194
227
|
const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
|
|
195
228
|
const folderColors = useMemo(
|
|
196
229
|
() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])),
|
|
197
|
-
[calendarFolders, colorFor]
|
|
230
|
+
[calendarFolders, colorFor]
|
|
198
231
|
);
|
|
199
232
|
|
|
200
233
|
function toggleCalendar(toggledUid: string) {
|
|
@@ -257,7 +290,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
257
290
|
}
|
|
258
291
|
setLoading(true);
|
|
259
292
|
setError(null);
|
|
260
|
-
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client)))
|
|
293
|
+
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client).then(openSealedEvents)))
|
|
261
294
|
.then((results) => {
|
|
262
295
|
if (seq !== reloadSeqRef.current) {
|
|
263
296
|
return;
|
|
@@ -295,7 +328,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
295
328
|
|
|
296
329
|
// Keyed on stable string summaries (not the array/set objects themselves, which are new references
|
|
297
330
|
// every render) so this only re-fires when the actual set of folders to fetch changes.
|
|
298
|
-
const calendarFolderUidsKey = calendarFolders
|
|
331
|
+
const calendarFolderUidsKey = calendarFolders
|
|
332
|
+
.map((f) => f.uid)
|
|
333
|
+
.sort()
|
|
334
|
+
.join(",");
|
|
299
335
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
300
336
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
301
337
|
|
|
@@ -317,23 +353,25 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
317
353
|
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
318
354
|
return;
|
|
319
355
|
}
|
|
320
|
-
void getCalendarEvent(uid, client)
|
|
321
|
-
(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
356
|
+
void getCalendarEvent(uid, client)
|
|
357
|
+
.then(openSealedEvent)
|
|
358
|
+
.then(
|
|
359
|
+
(fresh) => {
|
|
360
|
+
if (checkedFolderUidsRef.current.has(fresh.folderUid)) {
|
|
361
|
+
setEvents((previous) =>
|
|
362
|
+
previous.some((existing) => existing.uid === fresh.uid)
|
|
363
|
+
? previous.map((existing) => (existing.uid === fresh.uid ? fresh : existing))
|
|
364
|
+
: [...previous, fresh]
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
(err: unknown) => {
|
|
369
|
+
if (err instanceof ApiRequestError && err.status === 404) {
|
|
370
|
+
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
371
|
+
}
|
|
333
372
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
},
|
|
373
|
+
);
|
|
374
|
+
}
|
|
337
375
|
);
|
|
338
376
|
|
|
339
377
|
// Search: `searchText` is the box, `query` what it held once typing paused (or Enter was pressed) and is what is searched for. A search covers every
|
|
@@ -361,7 +399,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
361
399
|
|
|
362
400
|
// What an open event card steps through, in order: the matches while a search runs, else every event of the checked calendars (bounded as a
|
|
363
401
|
// search is). The List holds only what is from today on, so that is all its card steps through. Worked out only when something needs it.
|
|
364
|
-
const everything = useMemo(
|
|
402
|
+
const everything = useMemo(
|
|
403
|
+
() => (view === "list" || modal?.occurrence ? allOccurrences(events) : []),
|
|
404
|
+
[events, view, !!modal?.occurrence]
|
|
405
|
+
);
|
|
365
406
|
const sequence = useMemo(() => {
|
|
366
407
|
if (searching) {
|
|
367
408
|
return matches;
|
|
@@ -377,7 +418,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
377
418
|
if (view === "list") {
|
|
378
419
|
return sequence;
|
|
379
420
|
}
|
|
380
|
-
return searching
|
|
421
|
+
return searching
|
|
422
|
+
? occurrencesInRange(matches, rangeStart, rangeEnd)
|
|
423
|
+
: expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
381
424
|
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
382
425
|
|
|
383
426
|
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
@@ -397,7 +440,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
397
440
|
// calendar do not.
|
|
398
441
|
const stepFrom = modal?.occurrence;
|
|
399
442
|
const stepIndex = (direction: 1 | -1) =>
|
|
400
|
-
stepSequenceIndex(
|
|
443
|
+
stepSequenceIndex(
|
|
444
|
+
sequence,
|
|
445
|
+
stepFrom?.occurrenceKey ?? activeKey,
|
|
446
|
+
stepFrom ? new Date(stepFrom.startDate) : undefined,
|
|
447
|
+
direction,
|
|
448
|
+
searching
|
|
449
|
+
);
|
|
401
450
|
|
|
402
451
|
function stepMatch(direction: 1 | -1) {
|
|
403
452
|
const next = sequence[stepIndex(direction)];
|
|
@@ -433,17 +482,22 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
433
482
|
searchedQueryRef.current = query;
|
|
434
483
|
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
435
484
|
const from = startOfDay(viewDate);
|
|
436
|
-
showMatch(
|
|
485
|
+
showMatch(
|
|
486
|
+
matches[
|
|
487
|
+
view === "list"
|
|
488
|
+
? initialMatchIndex(matches, from, rangeEnd, from)
|
|
489
|
+
: initialMatchIndex(matches, rangeStart, rangeEnd, from)
|
|
490
|
+
]
|
|
491
|
+
);
|
|
437
492
|
}
|
|
438
493
|
}, [matches, query, searching]);
|
|
439
494
|
|
|
440
|
-
|
|
441
495
|
const splitColumns = useMemo(
|
|
442
496
|
() =>
|
|
443
497
|
calendarFolders
|
|
444
498
|
.filter((f) => checkedFolderUids.has(f.uid))
|
|
445
499
|
.map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })),
|
|
446
|
-
[calendarFolders, checkedFolderUidsKey, colorFor]
|
|
500
|
+
[calendarFolders, checkedFolderUidsKey, colorFor]
|
|
447
501
|
);
|
|
448
502
|
|
|
449
503
|
function shiftView(direction: 1 | -1) {
|
|
@@ -457,7 +511,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
457
511
|
? addMonths(d, direction)
|
|
458
512
|
: view === "week" || view === "workWeek"
|
|
459
513
|
? addWeeks(d, direction)
|
|
460
|
-
: addDays(d, direction)
|
|
514
|
+
: addDays(d, direction)
|
|
461
515
|
);
|
|
462
516
|
}
|
|
463
517
|
|
|
@@ -476,7 +530,8 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
476
530
|
const wheelSlide = useEnterSlide();
|
|
477
531
|
/** Where the scroller of the page a wheel step arrived at starts: its top after going on, its bottom after going back. */
|
|
478
532
|
const arrivalEdge = useRef<WheelEdge | null>(null);
|
|
479
|
-
const scrollerOf = (): HTMLElement | null =>
|
|
533
|
+
const scrollerOf = (): HTMLElement | null =>
|
|
534
|
+
swipe.ref.current?.querySelector<HTMLElement>("[data-calendar-scroller]") ?? null;
|
|
480
535
|
useWheelPaging(swipe.ref, {
|
|
481
536
|
enabled: !isMobile && !dragging && !modal && view !== "list",
|
|
482
537
|
getScroller: () => (view === "month" ? null : scrollerOf()),
|
|
@@ -498,7 +553,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
498
553
|
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
499
554
|
useEffect(() => {
|
|
500
555
|
const marked = Array.from(swipe.ref.current!.querySelectorAll<HTMLElement>("[data-occurrence-key]"));
|
|
501
|
-
marked
|
|
556
|
+
marked
|
|
557
|
+
.find((el) => el.dataset.occurrenceKey === activeKey)
|
|
558
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
502
559
|
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
503
560
|
|
|
504
561
|
// The List scrolls to `viewDate` when this changes (Today, a day picked in the mini calendar), even when it is the date it already has.
|
|
@@ -514,7 +571,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
514
571
|
setViewDate(startOfDay(date));
|
|
515
572
|
}
|
|
516
573
|
|
|
517
|
-
function openNewEvent(
|
|
574
|
+
function openNewEvent(
|
|
575
|
+
start?: Date,
|
|
576
|
+
end?: Date,
|
|
577
|
+
targetFolderUid?: string,
|
|
578
|
+
anchor?: EventAnchor,
|
|
579
|
+
initialAllDay?: boolean
|
|
580
|
+
) {
|
|
518
581
|
if (!mailboxUid || !folderUid) {
|
|
519
582
|
return;
|
|
520
583
|
}
|
|
@@ -558,10 +621,17 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
558
621
|
const requestedStart = new Date(params.get("start") ?? "");
|
|
559
622
|
navigateTo("/calendar", { replace: true });
|
|
560
623
|
getCalendarEvent(eventUid, client)
|
|
624
|
+
.then(openSealedEvent)
|
|
561
625
|
.then((event) => {
|
|
562
626
|
const from = isNaN(requestedStart.getTime()) ? new Date(event.startDate) : requestedStart;
|
|
563
|
-
const candidates = expandOccurrences(
|
|
564
|
-
|
|
627
|
+
const candidates = expandOccurrences(
|
|
628
|
+
event,
|
|
629
|
+
new Date(from.getTime() - 86_400_000),
|
|
630
|
+
new Date(from.getTime() + 86_400_000)
|
|
631
|
+
);
|
|
632
|
+
const match =
|
|
633
|
+
candidates.find((occurrence) => new Date(occurrence.startDate).getTime() === from.getTime()) ??
|
|
634
|
+
candidates[0];
|
|
565
635
|
if (match) {
|
|
566
636
|
setViewDate(occurrenceDay(match));
|
|
567
637
|
openEvent(match);
|
|
@@ -625,9 +695,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
625
695
|
? "Upcoming events"
|
|
626
696
|
: view === "month"
|
|
627
697
|
? format(viewDate, "MMMM yyyy")
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
698
|
+
: view === "week" || view === "workWeek"
|
|
699
|
+
? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
|
|
700
|
+
: format(viewDate, "EEEE, MMMM d, yyyy");
|
|
631
701
|
|
|
632
702
|
const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
|
|
633
703
|
const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
|
|
@@ -638,9 +708,14 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
638
708
|
const mailboxOptions = useMemo(
|
|
639
709
|
() =>
|
|
640
710
|
mailboxCalendars
|
|
641
|
-
.filter(
|
|
642
|
-
|
|
643
|
-
|
|
711
|
+
.filter(
|
|
712
|
+
(mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid)
|
|
713
|
+
)
|
|
714
|
+
.map((mc) => ({
|
|
715
|
+
mailbox: mc.mailbox,
|
|
716
|
+
calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })),
|
|
717
|
+
})),
|
|
718
|
+
[mailboxCalendars, writableMailboxes]
|
|
644
719
|
);
|
|
645
720
|
|
|
646
721
|
const sidebarContent = (
|
|
@@ -664,7 +739,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
664
739
|
|
|
665
740
|
return (
|
|
666
741
|
<div className="flex-1 min-w-0 flex min-h-0">
|
|
667
|
-
<div className="hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto">
|
|
742
|
+
<div className="hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto">
|
|
743
|
+
{sidebarContent}
|
|
744
|
+
</div>
|
|
668
745
|
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Calendars" fullScreen>
|
|
669
746
|
<div className="flex flex-col">{sidebarContent}</div>
|
|
670
747
|
</Drawer>
|
|
@@ -681,18 +758,42 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
681
758
|
</button>
|
|
682
759
|
{/* On a phone New event is the floating button at the bottom of the page instead, as in Mail. */}
|
|
683
760
|
{!isMobile && (
|
|
684
|
-
<Button
|
|
761
|
+
<Button
|
|
762
|
+
type="button"
|
|
763
|
+
onClick={(e) =>
|
|
764
|
+
openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below"))
|
|
765
|
+
}
|
|
766
|
+
className="!w-auto shrink-0"
|
|
767
|
+
{...newEventHint}
|
|
768
|
+
>
|
|
685
769
|
+ New event
|
|
686
770
|
</Button>
|
|
687
771
|
)}
|
|
688
772
|
<div className="flex items-center gap-1">
|
|
689
|
-
<button
|
|
773
|
+
<button
|
|
774
|
+
type="button"
|
|
775
|
+
onClick={() => shiftView(-1)}
|
|
776
|
+
aria-label="Previous"
|
|
777
|
+
{...previousHint}
|
|
778
|
+
className="w-7 h-7 text-sm rounded-sm hover:bg-surface-alt"
|
|
779
|
+
>
|
|
690
780
|
‹
|
|
691
781
|
</button>
|
|
692
|
-
<button
|
|
782
|
+
<button
|
|
783
|
+
type="button"
|
|
784
|
+
onClick={goToday}
|
|
785
|
+
{...todayHint}
|
|
786
|
+
className="px-2 h-7 text-sm rounded-sm hover:bg-surface-alt"
|
|
787
|
+
>
|
|
693
788
|
Today
|
|
694
789
|
</button>
|
|
695
|
-
<button
|
|
790
|
+
<button
|
|
791
|
+
type="button"
|
|
792
|
+
onClick={() => shiftView(1)}
|
|
793
|
+
aria-label="Next"
|
|
794
|
+
{...nextHint}
|
|
795
|
+
className="w-7 h-7 text-sm rounded-sm hover:bg-surface-alt"
|
|
796
|
+
>
|
|
696
797
|
›
|
|
697
798
|
</button>
|
|
698
799
|
</div>
|
|
@@ -722,8 +823,17 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
722
823
|
</div>
|
|
723
824
|
)}
|
|
724
825
|
|
|
725
|
-
<div
|
|
726
|
-
|
|
826
|
+
<div
|
|
827
|
+
ref={swipe.ref}
|
|
828
|
+
className="flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden"
|
|
829
|
+
{...swipe.handlers}
|
|
830
|
+
style={swipe.style}
|
|
831
|
+
>
|
|
832
|
+
<div
|
|
833
|
+
data-swipe-phase={swipe.phase}
|
|
834
|
+
className="flex-1 min-w-0 flex flex-col min-h-0"
|
|
835
|
+
style={swipe.contentStyle}
|
|
836
|
+
>
|
|
727
837
|
<div className="flex-1 min-w-0 flex flex-col min-h-0" style={wheelSlide.style}>
|
|
728
838
|
{loading ? (
|
|
729
839
|
<p className="p-4 text-sm text-text-muted">Loading…</p>
|
|
@@ -751,7 +861,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
751
861
|
folderColors={folderColors}
|
|
752
862
|
onSelectDay={handleSelectDay}
|
|
753
863
|
onSelectEvent={openEvent}
|
|
754
|
-
onSelectSlot={(start, end, anchor) =>
|
|
864
|
+
onSelectSlot={(start, end, anchor) =>
|
|
865
|
+
openNewEvent(start, end, undefined, anchor, true)
|
|
866
|
+
}
|
|
755
867
|
/>
|
|
756
868
|
) : view === "split" ? (
|
|
757
869
|
<SplitDayView
|
|
@@ -759,7 +871,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
759
871
|
columns={splitColumns}
|
|
760
872
|
occurrences={occurrences}
|
|
761
873
|
onSelectEvent={openEvent}
|
|
762
|
-
onSelectSlot={(start, end, targetFolderUid, anchor) =>
|
|
874
|
+
onSelectSlot={(start, end, targetFolderUid, anchor) =>
|
|
875
|
+
openNewEvent(start, end, targetFolderUid, anchor)
|
|
876
|
+
}
|
|
763
877
|
/>
|
|
764
878
|
) : (
|
|
765
879
|
<TimeGridView
|
|
@@ -767,7 +881,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
767
881
|
occurrences={occurrences}
|
|
768
882
|
folderColors={folderColors}
|
|
769
883
|
onSelectEvent={openEvent}
|
|
770
|
-
onSelectSlot={(start, end, anchor) =>
|
|
884
|
+
onSelectSlot={(start, end, anchor) =>
|
|
885
|
+
openNewEvent(start, end, undefined, anchor)
|
|
886
|
+
}
|
|
771
887
|
/>
|
|
772
888
|
)}
|
|
773
889
|
</DndContext>
|
|
@@ -787,7 +903,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
787
903
|
onClose={closeModal}
|
|
788
904
|
mailboxUid={modalMailbox.uid}
|
|
789
905
|
folderUid={modalFolderUid!}
|
|
790
|
-
calendars={calendarFolders
|
|
906
|
+
calendars={calendarFolders
|
|
907
|
+
.filter((f) => f.mailboxUid === modalMailbox.uid)
|
|
908
|
+
.map((f) => ({ uid: f.uid, name: f.name }))}
|
|
791
909
|
mailboxOptions={mailboxOptions}
|
|
792
910
|
folderColors={folderColors}
|
|
793
911
|
organizerAddress={modalMailbox.primarySmtpAddress}
|
|
@@ -816,7 +934,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
816
934
|
)}
|
|
817
935
|
{/* The phone layout's New event: the old toolbar button's action (its quick form is a bottom sheet there, so no anchor), offered
|
|
818
936
|
only where that button could act - a calendar to put the event in - and not while the editor or its sheet is open. */}
|
|
819
|
-
{isMobile && mailboxUid && folderUid && !modal &&
|
|
937
|
+
{isMobile && mailboxUid && folderUid && !modal && (
|
|
938
|
+
<FloatingActionButton label="New event" icon={HiOutlinePlus} onClick={() => openNewEvent()} />
|
|
939
|
+
)}
|
|
820
940
|
</div>
|
|
821
941
|
</div>
|
|
822
942
|
);
|
package/apps/www/index.tsx
CHANGED
|
@@ -160,11 +160,18 @@ async function decryptEncryptedRows(messages: Message[], unlocked: UnlockedKeys)
|
|
|
160
160
|
DECRYPT_CONCURRENCY,
|
|
161
161
|
async (message): Promise<[string, DecryptedRow] | null> => {
|
|
162
162
|
try {
|
|
163
|
-
const rawMime = await getMessageRawContent(message.uid);
|
|
164
163
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
165
164
|
// no encrypted mail in it never needs it.
|
|
166
|
-
const
|
|
167
|
-
|
|
165
|
+
const securityApi = await (securityModule ??= import("../../lib/crypto/messageSecurity.js"));
|
|
166
|
+
// A message sealed at rest carries its subject and preview sealed on the row: no download needed.
|
|
167
|
+
const summary = message.zeroAccess ? await securityApi.openZeroAccessSummary(message.zeroAccess.sealedSummary, unlocked.zeroAccessKeys) : undefined;
|
|
168
|
+
if (summary) {
|
|
169
|
+
const preview = summary.preview.slice(0, LIST_PREVIEW_MAX_LENGTH);
|
|
170
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: summary.subject, preview });
|
|
171
|
+
return [message.uid, { subject: summary.subject, preview }];
|
|
172
|
+
}
|
|
173
|
+
const rawMime = await getMessageRawContent(message.uid);
|
|
174
|
+
const security = await securityApi.evaluateMessageSecurity(rawMime, unlocked);
|
|
168
175
|
if (!security.subject && !security.html) {
|
|
169
176
|
markDecryptFailed(message.uid);
|
|
170
177
|
return null;
|
|
@@ -51,6 +51,8 @@ import { notifyApiError } from "../../../shared/notifications/apiErrors.js";
|
|
|
51
51
|
import Alert from "../../../../lib/components/feedback/Alert.js";
|
|
52
52
|
import Button from "../../../../lib/components/buttons/Button.js";
|
|
53
53
|
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
54
|
+
import ZeroAccessSetting from "../../../shared/components/settings/ZeroAccessSetting.js";
|
|
55
|
+
import KeyExportSetting from "../../../shared/components/settings/KeyExportSetting.js";
|
|
54
56
|
import SigningCertificateCard, { SigningCertificateMode } from "../../../shared/components/settings/SigningCertificateCard.js";
|
|
55
57
|
import { readStoredSignEnrollment, storeSignEnrollment } from "../../../shared/signing/enrollmentStorage.js";
|
|
56
58
|
import {
|
|
@@ -975,6 +977,9 @@ function EncryptionContent({ canManageKeys }: { canManageKeys: boolean }) {
|
|
|
975
977
|
)}
|
|
976
978
|
</div>
|
|
977
979
|
|
|
980
|
+
<ZeroAccessSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
981
|
+
<KeyExportSetting mailbox={mailbox} canManage={canManageKeys} />
|
|
982
|
+
|
|
978
983
|
<div>
|
|
979
984
|
<h2 className="text-sm font-semibold mb-2">Digital signatures</h2>
|
|
980
985
|
{activeSigningKey && signingStatus !== "pending" && (
|
|
@@ -174,7 +174,8 @@ function ExportSection({ mailboxUid, mailboxLabel }: { mailboxUid?: string; mail
|
|
|
174
174
|
<p className="text-xs text-text-muted mb-3">
|
|
175
175
|
Requests a copy of <strong>{mailboxLabel}</strong>’s messages, contacts, calendar, tasks, and notes. This runs
|
|
176
176
|
in the background and can take a few minutes — check back here for a download link once it’s
|
|
177
|
-
ready.
|
|
177
|
+
ready. Mail stored with zero-access encryption is exported encrypted: open it in a mail app that has your
|
|
178
|
+
exported keys (Settings › Encryption).
|
|
178
179
|
</p>
|
|
179
180
|
|
|
180
181
|
<form onSubmit={handleCreate} className="flex items-end gap-3 mb-5">
|
|
@@ -182,7 +182,7 @@ function MatterDetailContent({ uid }) {
|
|
|
182
182
|
try {
|
|
183
183
|
const material = await getAccessRequestMaterial(request.uid);
|
|
184
184
|
if (seq === materialSeq.current)
|
|
185
|
-
setMaterialText(JSON.stringify(material
|
|
185
|
+
setMaterialText(JSON.stringify(material, null, 2));
|
|
186
186
|
}
|
|
187
187
|
catch (err) {
|
|
188
188
|
if (seq === materialSeq.current) {
|
|
@@ -270,5 +270,5 @@ function MatterDetailContent({ uid }) {
|
|
|
270
270
|
? "bg-surface-alt text-text-muted"
|
|
271
271
|
: "bg-success text-white",
|
|
272
272
|
].join(" "), children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), _jsxs("div", { className: "flex gap-3 mt-3", children: [!matter.closedAt && request.status === "pending" && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setDenyTarget(request), children: "Deny" })] })), !matter.closedAt && (request.status === "approved" || request.status === "fulfilled") && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => handleGetMaterial(request), children: "Get material" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: requests, label: "Load more access requests" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide", children: "Export this matter" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", loading: creatingExport, disabled: creatingExport, onClick: handleCreateExport, children: "+ New export" }))] }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Exports messages (narrowed to this matter\u2019s own date range), plus full contacts, calendar events, tasks, and notes, from every custodian mailbox actually assigned to this matter\u2019s escrow scope, as one combined NDJSON file." }), exportError && _jsx(Alert, { children: exportError }), exportRequests.loadError && _jsx(Alert, { children: exportRequests.loadError }), matterExportRequests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: exportRequests.loading ? "Loading…" : "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: matterExportRequests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), !matter.closedAt && request.status === "ready" && (_jsx("a", { href: matterExportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-3", children: "Search this matter\u2019s custodians" }), matter.closedAt ? (_jsx("p", { className: "text-sm text-text-muted", children: "This matter is closed, so its custodians can no longer be searched or exported." })) : (_jsxs("form", { onSubmit: handleSearch, className: "flex gap-3 mb-4", children: [_jsx("input", { "aria-label": "Search this matter", className: INPUT_CLASS, value: searchText, onChange: (e) => setSearchText(e.target.value), placeholder: "Free text or an operator filter, e.g. from:alice@example.com" }), _jsx(Button, { type: "submit", className: "!w-auto shrink-0", loading: searching, disabled: searching || !searchText.trim(), children: "Search" })] })), searchError && _jsx(Alert, { children: searchError }), searchResults &&
|
|
273
|
-
(Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: () => setDenyTarget(null), title: "Deny access request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Denying refuses this request for good: it can\u2019t be approved afterwards, and holders can\u2019t read the key material for mailbox ", _jsx("span", { className: "break-all font-semibold", children: denyTarget?.mailboxUid }), " through it. A new request has to be made to ask again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleDeny(), children: "Deny request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted
|
|
273
|
+
(Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: () => setDenyTarget(null), title: "Deny access request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Denying refuses this request for good: it can\u2019t be approved afterwards, and holders can\u2019t read the key material for mailbox ", _jsx("span", { className: "break-all font-semibold", children: denyTarget?.mailboxUid }), " through it. A new request has to be made to ask again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleDeny(), children: "Deny request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted key material for ", _jsx("strong", { children: materialFor?.mailboxUid }), ": the escrow master key wrap, and the private keys that read its mail, sealed under that master key. This server cannot decrypt any of it either. Copy the JSON below into your own offline tooling."] }), _jsx("pre", { className: "text-xs font-mono bg-surface-alt border border-border rounded-sm p-3 overflow-x-auto whitespace-pre-wrap break-all", children: materialText })] })) })] }));
|
|
274
274
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar events the server made from invitations that reached a zero-access mailbox keep their title, location and description only
|
|
3
|
+
* sealed to the mailbox's zero-access key (`CalendarEvent.sealedContent`; the row's `title` is `[...]`). These are opened here with the
|
|
4
|
+
* mailbox's unlocked keys so the calendar shows the real ones; while the mailbox is locked the event keeps its placeholder. The
|
|
5
|
+
* decryption code is loaded only when a sealed event is actually seen.
|
|
6
|
+
*/
|
|
7
|
+
import type { CalendarEvent } from "../../../lib/calendar/calendarApi.js";
|
|
8
|
+
/** Forgets every opened event (sign-out, or the test seam). */
|
|
9
|
+
export declare function clearSealedEvents(): void;
|
|
10
|
+
/** `events` with every sealed one this device can open shown with its real title, location and description. Never rejects. */
|
|
11
|
+
export declare function openSealedEvents(events: CalendarEvent[]): Promise<CalendarEvent[]>;
|
|
12
|
+
/** `openSealedEvents()` for one event. */
|
|
13
|
+
export declare function openSealedEvent(event: CalendarEvent): Promise<CalendarEvent>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getUnlockedKeys } from "../../../lib/crypto/keySession.js";
|
|
2
|
+
import { fromBase64 } from "../../../lib/crypto/encoding.js";
|
|
3
|
+
import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
|
|
4
|
+
/** Opened fields by sealed text - the same sealed content always opens to the same fields. Cleared with `clearSealedEvents()`. */
|
|
5
|
+
const opened = new Map();
|
|
6
|
+
/** Forgets every opened event (sign-out, or the test seam). */
|
|
7
|
+
export function clearSealedEvents() {
|
|
8
|
+
opened.clear();
|
|
9
|
+
}
|
|
10
|
+
async function openFields(event) {
|
|
11
|
+
const sealed = event.sealedContent.sealed;
|
|
12
|
+
const known = opened.get(sealed);
|
|
13
|
+
if (known) {
|
|
14
|
+
return known;
|
|
15
|
+
}
|
|
16
|
+
const keys = getUnlockedKeys(event.mailboxUid)?.zeroAccessKeys;
|
|
17
|
+
if (!keys?.length) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const { decryptEnvelopedDataWithKeys } = await import("../../../lib/crypto/smime.js");
|
|
22
|
+
const fields = JSON.parse(new TextDecoder().decode(await decryptEnvelopedDataWithKeys(fromBase64(sealed), keys)));
|
|
23
|
+
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
24
|
+
const result = {
|
|
25
|
+
title: text(fields.title) ?? event.title,
|
|
26
|
+
location: text(fields.location),
|
|
27
|
+
description: text(fields.description) ?? null,
|
|
28
|
+
descriptionHtml: text(fields.descriptionHtml) ?? null,
|
|
29
|
+
};
|
|
30
|
+
opened.set(sealed, result);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** `events` with every sealed one this device can open shown with its real title, location and description. Never rejects. */
|
|
38
|
+
export async function openSealedEvents(events) {
|
|
39
|
+
if (!events.some((event) => event.sealedContent)) {
|
|
40
|
+
return events;
|
|
41
|
+
}
|
|
42
|
+
return Promise.all(events.map(async (event) => {
|
|
43
|
+
if (!event.sealedContent) {
|
|
44
|
+
return event;
|
|
45
|
+
}
|
|
46
|
+
const fields = await openFields(event);
|
|
47
|
+
return fields ? { ...event, ...fields } : event;
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
/** `openSealedEvents()` for one event. */
|
|
51
|
+
export async function openSealedEvent(event) {
|
|
52
|
+
return (await openSealedEvents([event]))[0];
|
|
53
|
+
}
|
|
54
|
+
onSigningOut(clearSealedEvents);
|