@rapidmx/web-client 0.34.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/calendarLiveUpdates.ts +2 -5
- package/apps/shared/calendar/sealedEvents.ts +80 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +61 -3
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +8 -4
- 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 +91 -20
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/quotedBody.ts +31 -15
- 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/MessageBody.tsx +4 -1
- package/apps/shared/components/mail/reading/bodyContent.ts +27 -17
- package/apps/shared/components/mail/reading/conversationPrint.ts +7 -5
- 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/live/useFolderLiveUpdates.ts +59 -0
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +2 -2
- package/apps/shared/mail/outbox/sendOutcomes.ts +1 -3
- 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 +180 -58
- package/apps/www/contacts/index.tsx +58 -2
- 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/apps/www/tasks/index.tsx +42 -0
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +2 -5
- 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/ContactsSidebar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/contactCardData.d.ts +2 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
- 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 +73 -17
- 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 +4 -3
- package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -15
- 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/MessageBody.js +4 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +4 -2
- package/dist/apps/shared/components/mail/reading/bodyContent.js +27 -16
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +3 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +7 -5
- 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/live/useFolderLiveUpdates.d.ts +19 -0
- package/dist/apps/shared/live/useFolderLiveUpdates.js +41 -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/outbox/sendOutcomes.js +1 -3
- 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 +48 -22
- package/dist/apps/www/contacts/index.js +49 -3
- 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/apps/www/tasks/index.js +29 -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/mail/pushClient.d.ts +10 -3
- package/dist/lib/mail/pushClient.js +18 -4
- package/dist/lib/search/zeroAccessIndexApi.d.ts +81 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/dist/lib/tasks/tasksApi.d.ts +1 -0
- package/dist/lib/tasks/tasksApi.js +3 -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/mail/pushClient.ts +18 -4
- package/lib/search/zeroAccessIndexApi.ts +119 -0
- package/lib/tasks/tasksApi.ts +4 -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 {
|
|
28
|
+
import { openSealedEvent, openSealedEvents } from "../../shared/calendar/sealedEvents.js";
|
|
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";
|
|
@@ -63,6 +80,9 @@ import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
|
63
80
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
64
81
|
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
65
82
|
|
|
83
|
+
/** The push client channel group (`PushClient.setChannels()`) the page's calendars are subscribed under. */
|
|
84
|
+
const CALENDAR_CHANNEL_GROUP = "calendar";
|
|
85
|
+
|
|
66
86
|
/** The furthest instants a `Date` can hold: the List's "range" reaches both. */
|
|
67
87
|
const MAX_DATE_MS = 8.64e15;
|
|
68
88
|
|
|
@@ -75,7 +95,14 @@ function CalendarPage(props: CalendarShellProps) {
|
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
type ViewType = CalendarView;
|
|
78
|
-
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
|
+
};
|
|
79
106
|
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
80
107
|
const VIEW_SHORTCUTS: Partial<Record<ViewType, ShortcutDef>> = {
|
|
81
108
|
list: SHORTCUTS.calendar.list,
|
|
@@ -86,7 +113,15 @@ const VIEW_SHORTCUTS: Partial<Record<ViewType, ShortcutDef>> = {
|
|
|
86
113
|
};
|
|
87
114
|
|
|
88
115
|
/** One of the view switcher's buttons, with its shortcut in the tooltip. */
|
|
89
|
-
function ViewButton({
|
|
116
|
+
function ViewButton({
|
|
117
|
+
view,
|
|
118
|
+
current,
|
|
119
|
+
onSelect,
|
|
120
|
+
}: {
|
|
121
|
+
view: ViewType;
|
|
122
|
+
current: ViewType;
|
|
123
|
+
onSelect: (view: ViewType) => void;
|
|
124
|
+
}) {
|
|
90
125
|
const shortcut = VIEW_SHORTCUTS[view];
|
|
91
126
|
const hint = useShortcutProps(VIEW_LABELS[view], shortcut ?? SHORTCUTS.calendar.day, !!shortcut);
|
|
92
127
|
return (
|
|
@@ -121,7 +156,8 @@ interface ModalState {
|
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHref?: string }) {
|
|
124
|
-
const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } =
|
|
159
|
+
const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } =
|
|
160
|
+
useCalendarShell();
|
|
125
161
|
const client = useApiClient();
|
|
126
162
|
// `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
|
|
127
163
|
// scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
|
|
@@ -130,7 +166,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
130
166
|
// dnd-kit's own documented pattern for this exact conflict.
|
|
131
167
|
const sensors = useSensors(
|
|
132
168
|
useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),
|
|
133
|
-
useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })
|
|
169
|
+
useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })
|
|
134
170
|
);
|
|
135
171
|
|
|
136
172
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
@@ -191,7 +227,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
191
227
|
const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
|
|
192
228
|
const folderColors = useMemo(
|
|
193
229
|
() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])),
|
|
194
|
-
[calendarFolders, colorFor]
|
|
230
|
+
[calendarFolders, colorFor]
|
|
195
231
|
);
|
|
196
232
|
|
|
197
233
|
function toggleCalendar(toggledUid: string) {
|
|
@@ -254,7 +290,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
254
290
|
}
|
|
255
291
|
setLoading(true);
|
|
256
292
|
setError(null);
|
|
257
|
-
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client)))
|
|
293
|
+
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client).then(openSealedEvents)))
|
|
258
294
|
.then((results) => {
|
|
259
295
|
if (seq !== reloadSeqRef.current) {
|
|
260
296
|
return;
|
|
@@ -292,7 +328,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
292
328
|
|
|
293
329
|
// Keyed on stable string summaries (not the array/set objects themselves, which are new references
|
|
294
330
|
// every render) so this only re-fires when the actual set of folders to fetch changes.
|
|
295
|
-
const calendarFolderUidsKey = calendarFolders
|
|
331
|
+
const calendarFolderUidsKey = calendarFolders
|
|
332
|
+
.map((f) => f.uid)
|
|
333
|
+
.sort()
|
|
334
|
+
.join(",");
|
|
296
335
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
297
336
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
298
337
|
|
|
@@ -302,25 +341,27 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
302
341
|
// fetch changes nothing: the next load shows the event as it is.
|
|
303
342
|
const checkedFolderUidsRef = useRef(checkedFolderUids);
|
|
304
343
|
checkedFolderUidsRef.current = checkedFolderUids;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
// Those events are published on the calendar's own channel, so the page subscribes to every calendar it lists - checked or not, so checking
|
|
345
|
+
// one needs no new subscription - while it is open.
|
|
346
|
+
useFolderLiveUpdates(
|
|
347
|
+
CALENDAR_CHANNEL_GROUP,
|
|
348
|
+
calendarFolders.map((f) => f.uid),
|
|
349
|
+
changedEventOf,
|
|
350
|
+
(change) => {
|
|
351
|
+
const { uid } = change;
|
|
352
|
+
if (change.deleted) {
|
|
353
|
+
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
void getCalendarEvent(uid, client)
|
|
357
|
+
.then(openSealedEvent)
|
|
358
|
+
.then(
|
|
318
359
|
(fresh) => {
|
|
319
360
|
if (checkedFolderUidsRef.current.has(fresh.folderUid)) {
|
|
320
361
|
setEvents((previous) =>
|
|
321
362
|
previous.some((existing) => existing.uid === fresh.uid)
|
|
322
363
|
? previous.map((existing) => (existing.uid === fresh.uid ? fresh : existing))
|
|
323
|
-
: [...previous, fresh]
|
|
364
|
+
: [...previous, fresh]
|
|
324
365
|
);
|
|
325
366
|
}
|
|
326
367
|
},
|
|
@@ -328,10 +369,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
328
369
|
if (err instanceof ApiRequestError && err.status === 404) {
|
|
329
370
|
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
330
371
|
}
|
|
331
|
-
}
|
|
372
|
+
}
|
|
332
373
|
);
|
|
333
|
-
|
|
334
|
-
[client],
|
|
374
|
+
}
|
|
335
375
|
);
|
|
336
376
|
|
|
337
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
|
|
@@ -359,7 +399,10 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
359
399
|
|
|
360
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
|
|
361
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.
|
|
362
|
-
const everything = useMemo(
|
|
402
|
+
const everything = useMemo(
|
|
403
|
+
() => (view === "list" || modal?.occurrence ? allOccurrences(events) : []),
|
|
404
|
+
[events, view, !!modal?.occurrence]
|
|
405
|
+
);
|
|
363
406
|
const sequence = useMemo(() => {
|
|
364
407
|
if (searching) {
|
|
365
408
|
return matches;
|
|
@@ -375,7 +418,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
375
418
|
if (view === "list") {
|
|
376
419
|
return sequence;
|
|
377
420
|
}
|
|
378
|
-
return searching
|
|
421
|
+
return searching
|
|
422
|
+
? occurrencesInRange(matches, rangeStart, rangeEnd)
|
|
423
|
+
: expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
379
424
|
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
380
425
|
|
|
381
426
|
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
@@ -395,7 +440,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
395
440
|
// calendar do not.
|
|
396
441
|
const stepFrom = modal?.occurrence;
|
|
397
442
|
const stepIndex = (direction: 1 | -1) =>
|
|
398
|
-
stepSequenceIndex(
|
|
443
|
+
stepSequenceIndex(
|
|
444
|
+
sequence,
|
|
445
|
+
stepFrom?.occurrenceKey ?? activeKey,
|
|
446
|
+
stepFrom ? new Date(stepFrom.startDate) : undefined,
|
|
447
|
+
direction,
|
|
448
|
+
searching
|
|
449
|
+
);
|
|
399
450
|
|
|
400
451
|
function stepMatch(direction: 1 | -1) {
|
|
401
452
|
const next = sequence[stepIndex(direction)];
|
|
@@ -431,17 +482,22 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
431
482
|
searchedQueryRef.current = query;
|
|
432
483
|
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
433
484
|
const from = startOfDay(viewDate);
|
|
434
|
-
showMatch(
|
|
485
|
+
showMatch(
|
|
486
|
+
matches[
|
|
487
|
+
view === "list"
|
|
488
|
+
? initialMatchIndex(matches, from, rangeEnd, from)
|
|
489
|
+
: initialMatchIndex(matches, rangeStart, rangeEnd, from)
|
|
490
|
+
]
|
|
491
|
+
);
|
|
435
492
|
}
|
|
436
493
|
}, [matches, query, searching]);
|
|
437
494
|
|
|
438
|
-
|
|
439
495
|
const splitColumns = useMemo(
|
|
440
496
|
() =>
|
|
441
497
|
calendarFolders
|
|
442
498
|
.filter((f) => checkedFolderUids.has(f.uid))
|
|
443
499
|
.map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })),
|
|
444
|
-
[calendarFolders, checkedFolderUidsKey, colorFor]
|
|
500
|
+
[calendarFolders, checkedFolderUidsKey, colorFor]
|
|
445
501
|
);
|
|
446
502
|
|
|
447
503
|
function shiftView(direction: 1 | -1) {
|
|
@@ -455,7 +511,7 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
455
511
|
? addMonths(d, direction)
|
|
456
512
|
: view === "week" || view === "workWeek"
|
|
457
513
|
? addWeeks(d, direction)
|
|
458
|
-
: addDays(d, direction)
|
|
514
|
+
: addDays(d, direction)
|
|
459
515
|
);
|
|
460
516
|
}
|
|
461
517
|
|
|
@@ -474,7 +530,8 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
474
530
|
const wheelSlide = useEnterSlide();
|
|
475
531
|
/** Where the scroller of the page a wheel step arrived at starts: its top after going on, its bottom after going back. */
|
|
476
532
|
const arrivalEdge = useRef<WheelEdge | null>(null);
|
|
477
|
-
const scrollerOf = (): HTMLElement | null =>
|
|
533
|
+
const scrollerOf = (): HTMLElement | null =>
|
|
534
|
+
swipe.ref.current?.querySelector<HTMLElement>("[data-calendar-scroller]") ?? null;
|
|
478
535
|
useWheelPaging(swipe.ref, {
|
|
479
536
|
enabled: !isMobile && !dragging && !modal && view !== "list",
|
|
480
537
|
getScroller: () => (view === "month" ? null : scrollerOf()),
|
|
@@ -496,7 +553,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
496
553
|
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
497
554
|
useEffect(() => {
|
|
498
555
|
const marked = Array.from(swipe.ref.current!.querySelectorAll<HTMLElement>("[data-occurrence-key]"));
|
|
499
|
-
marked
|
|
556
|
+
marked
|
|
557
|
+
.find((el) => el.dataset.occurrenceKey === activeKey)
|
|
558
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
500
559
|
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
501
560
|
|
|
502
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.
|
|
@@ -512,7 +571,13 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
512
571
|
setViewDate(startOfDay(date));
|
|
513
572
|
}
|
|
514
573
|
|
|
515
|
-
function openNewEvent(
|
|
574
|
+
function openNewEvent(
|
|
575
|
+
start?: Date,
|
|
576
|
+
end?: Date,
|
|
577
|
+
targetFolderUid?: string,
|
|
578
|
+
anchor?: EventAnchor,
|
|
579
|
+
initialAllDay?: boolean
|
|
580
|
+
) {
|
|
516
581
|
if (!mailboxUid || !folderUid) {
|
|
517
582
|
return;
|
|
518
583
|
}
|
|
@@ -556,10 +621,17 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
556
621
|
const requestedStart = new Date(params.get("start") ?? "");
|
|
557
622
|
navigateTo("/calendar", { replace: true });
|
|
558
623
|
getCalendarEvent(eventUid, client)
|
|
624
|
+
.then(openSealedEvent)
|
|
559
625
|
.then((event) => {
|
|
560
626
|
const from = isNaN(requestedStart.getTime()) ? new Date(event.startDate) : requestedStart;
|
|
561
|
-
const candidates = expandOccurrences(
|
|
562
|
-
|
|
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];
|
|
563
635
|
if (match) {
|
|
564
636
|
setViewDate(occurrenceDay(match));
|
|
565
637
|
openEvent(match);
|
|
@@ -623,9 +695,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
623
695
|
? "Upcoming events"
|
|
624
696
|
: view === "month"
|
|
625
697
|
? format(viewDate, "MMMM yyyy")
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
698
|
+
: view === "week" || view === "workWeek"
|
|
699
|
+
? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
|
|
700
|
+
: format(viewDate, "EEEE, MMMM d, yyyy");
|
|
629
701
|
|
|
630
702
|
const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
|
|
631
703
|
const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
|
|
@@ -636,9 +708,14 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
636
708
|
const mailboxOptions = useMemo(
|
|
637
709
|
() =>
|
|
638
710
|
mailboxCalendars
|
|
639
|
-
.filter(
|
|
640
|
-
|
|
641
|
-
|
|
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]
|
|
642
719
|
);
|
|
643
720
|
|
|
644
721
|
const sidebarContent = (
|
|
@@ -662,7 +739,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
662
739
|
|
|
663
740
|
return (
|
|
664
741
|
<div className="flex-1 min-w-0 flex min-h-0">
|
|
665
|
-
<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>
|
|
666
745
|
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Calendars" fullScreen>
|
|
667
746
|
<div className="flex flex-col">{sidebarContent}</div>
|
|
668
747
|
</Drawer>
|
|
@@ -679,18 +758,42 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
679
758
|
</button>
|
|
680
759
|
{/* On a phone New event is the floating button at the bottom of the page instead, as in Mail. */}
|
|
681
760
|
{!isMobile && (
|
|
682
|
-
<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
|
+
>
|
|
683
769
|
+ New event
|
|
684
770
|
</Button>
|
|
685
771
|
)}
|
|
686
772
|
<div className="flex items-center gap-1">
|
|
687
|
-
<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
|
+
>
|
|
688
780
|
‹
|
|
689
781
|
</button>
|
|
690
|
-
<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
|
+
>
|
|
691
788
|
Today
|
|
692
789
|
</button>
|
|
693
|
-
<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
|
+
>
|
|
694
797
|
›
|
|
695
798
|
</button>
|
|
696
799
|
</div>
|
|
@@ -720,8 +823,17 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
720
823
|
</div>
|
|
721
824
|
)}
|
|
722
825
|
|
|
723
|
-
<div
|
|
724
|
-
|
|
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
|
+
>
|
|
725
837
|
<div className="flex-1 min-w-0 flex flex-col min-h-0" style={wheelSlide.style}>
|
|
726
838
|
{loading ? (
|
|
727
839
|
<p className="p-4 text-sm text-text-muted">Loading…</p>
|
|
@@ -749,7 +861,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
749
861
|
folderColors={folderColors}
|
|
750
862
|
onSelectDay={handleSelectDay}
|
|
751
863
|
onSelectEvent={openEvent}
|
|
752
|
-
onSelectSlot={(start, end, anchor) =>
|
|
864
|
+
onSelectSlot={(start, end, anchor) =>
|
|
865
|
+
openNewEvent(start, end, undefined, anchor, true)
|
|
866
|
+
}
|
|
753
867
|
/>
|
|
754
868
|
) : view === "split" ? (
|
|
755
869
|
<SplitDayView
|
|
@@ -757,7 +871,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
757
871
|
columns={splitColumns}
|
|
758
872
|
occurrences={occurrences}
|
|
759
873
|
onSelectEvent={openEvent}
|
|
760
|
-
onSelectSlot={(start, end, targetFolderUid, anchor) =>
|
|
874
|
+
onSelectSlot={(start, end, targetFolderUid, anchor) =>
|
|
875
|
+
openNewEvent(start, end, targetFolderUid, anchor)
|
|
876
|
+
}
|
|
761
877
|
/>
|
|
762
878
|
) : (
|
|
763
879
|
<TimeGridView
|
|
@@ -765,7 +881,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
765
881
|
occurrences={occurrences}
|
|
766
882
|
folderColors={folderColors}
|
|
767
883
|
onSelectEvent={openEvent}
|
|
768
|
-
onSelectSlot={(start, end, anchor) =>
|
|
884
|
+
onSelectSlot={(start, end, anchor) =>
|
|
885
|
+
openNewEvent(start, end, undefined, anchor)
|
|
886
|
+
}
|
|
769
887
|
/>
|
|
770
888
|
)}
|
|
771
889
|
</DndContext>
|
|
@@ -785,7 +903,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
785
903
|
onClose={closeModal}
|
|
786
904
|
mailboxUid={modalMailbox.uid}
|
|
787
905
|
folderUid={modalFolderUid!}
|
|
788
|
-
calendars={calendarFolders
|
|
906
|
+
calendars={calendarFolders
|
|
907
|
+
.filter((f) => f.mailboxUid === modalMailbox.uid)
|
|
908
|
+
.map((f) => ({ uid: f.uid, name: f.name }))}
|
|
789
909
|
mailboxOptions={mailboxOptions}
|
|
790
910
|
folderColors={folderColors}
|
|
791
911
|
organizerAddress={modalMailbox.primarySmtpAddress}
|
|
@@ -814,7 +934,9 @@ function CalendarContent({ userUid, bookingHref }: { userUid?: string; bookingHr
|
|
|
814
934
|
)}
|
|
815
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
|
|
816
936
|
only where that button could act - a calendar to put the event in - and not while the editor or its sheet is open. */}
|
|
817
|
-
{isMobile && mailboxUid && folderUid && !modal &&
|
|
937
|
+
{isMobile && mailboxUid && folderUid && !modal && (
|
|
938
|
+
<FloatingActionButton label="New event" icon={HiOutlinePlus} onClick={() => openNewEvent()} />
|
|
939
|
+
)}
|
|
818
940
|
</div>
|
|
819
941
|
</div>
|
|
820
942
|
);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Contact,
|
|
11
11
|
createContact,
|
|
12
12
|
deleteContact,
|
|
13
|
+
getContact,
|
|
13
14
|
listContacts,
|
|
14
15
|
ensureSuggestedContacts,
|
|
15
16
|
listContactsInFolders,
|
|
@@ -42,6 +43,18 @@ import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
|
42
43
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
43
44
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
44
45
|
import { notify } from "../../shared/notifications/store.js";
|
|
46
|
+
import { changedItemOf, useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
47
|
+
|
|
48
|
+
/** The published model class name of a contact on either database (`ContactMongo`, `ContactSQL`) - not a `ContactList`'s. */
|
|
49
|
+
const CONTACT_MODEL = /^Contact(Mongo|SQL)$/;
|
|
50
|
+
|
|
51
|
+
/** The push client channel group (`PushClient.setChannels()`) the page's contacts folders are subscribed under. */
|
|
52
|
+
const CONTACTS_CHANNEL_GROUP = "contacts";
|
|
53
|
+
|
|
54
|
+
/** `list` with `contact` in place of the one with its uid, or added when it has none. */
|
|
55
|
+
function upsertContact(list: Contact[], contact: Contact): Contact[] {
|
|
56
|
+
return list.some((existing) => existing.uid === contact.uid) ? list.map((existing) => (existing.uid === contact.uid ? contact : existing)) : [...list, contact];
|
|
57
|
+
}
|
|
45
58
|
|
|
46
59
|
const INPUT_CLASS =
|
|
47
60
|
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -166,6 +179,45 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
166
179
|
};
|
|
167
180
|
}, [mailboxUid]);
|
|
168
181
|
|
|
182
|
+
// A contact created, changed or deleted anywhere - another tab, a phone over ActiveSync - is announced on its folder's channel. It is fetched again by
|
|
183
|
+
// its uid and filed in the list of the folder it is in now (the mailbox's contacts, or its suggested contacts), and taken out of the other; one that is
|
|
184
|
+
// gone (a delete, or a 404) leaves both. A failed fetch changes nothing: the next load shows it as it is.
|
|
185
|
+
useFolderLiveUpdates(
|
|
186
|
+
CONTACTS_CHANNEL_GROUP,
|
|
187
|
+
[folderUid, suggestedFolderUid].filter((uid): uid is string => !!uid),
|
|
188
|
+
(event) => changedItemOf(event, CONTACT_MODEL),
|
|
189
|
+
(change) => {
|
|
190
|
+
const remove = (uid: string) => {
|
|
191
|
+
setContacts((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
192
|
+
setSuggestedContacts((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
193
|
+
};
|
|
194
|
+
if (change.deleted) {
|
|
195
|
+
remove(change.uid);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
void getContact(change.uid, client).then(
|
|
199
|
+
(fresh) => {
|
|
200
|
+
if (fresh.deleted === true) {
|
|
201
|
+
remove(fresh.uid);
|
|
202
|
+
} else if (fresh.folderUid === folderUid) {
|
|
203
|
+
setContacts((previous) => upsertContact(previous, fresh));
|
|
204
|
+
setSuggestedContacts((previous) => previous.filter((existing) => existing.uid !== fresh.uid));
|
|
205
|
+
} else if (fresh.folderUid === suggestedFolderUid) {
|
|
206
|
+
setSuggestedContacts((previous) => upsertContact(previous, fresh));
|
|
207
|
+
setContacts((previous) => previous.filter((existing) => existing.uid !== fresh.uid));
|
|
208
|
+
} else {
|
|
209
|
+
remove(fresh.uid);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
(err: unknown) => {
|
|
213
|
+
if (err instanceof ApiRequestError && err.status === 404) {
|
|
214
|
+
remove(change.uid);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
169
221
|
// restapi only honors `?deleted=true` for a caller with both delete and update rights on the contacts *folder*
|
|
170
222
|
// (its ACL, which inherits the mailbox's but can carry its own records) - anyone else silently gets the *live*
|
|
171
223
|
// contacts back, which the Deleted view would then present as deleted. There is no client API for the caller's
|
|
@@ -228,6 +280,9 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
228
280
|
return contacts;
|
|
229
281
|
case "favorites":
|
|
230
282
|
return contacts.filter((c) => c.favorite);
|
|
283
|
+
case "everyone":
|
|
284
|
+
// The mailbox's own contacts and its suggested ones in one list (a person is in only one of them: a suggestion that is accepted is moved).
|
|
285
|
+
return [...contacts, ...suggestedContacts];
|
|
231
286
|
case "suggested":
|
|
232
287
|
return suggestedContacts;
|
|
233
288
|
case "list":
|
|
@@ -428,7 +483,8 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
428
483
|
|
|
429
484
|
// A suggested contact becomes one of the mailbox's own: a copy in its contacts folder, and the suggestion removed (the person is not suggested again).
|
|
430
485
|
const handleAddToContacts = singleFlight(async () => {
|
|
431
|
-
|
|
486
|
+
// In the All contacts view the selection can hold contacts that already are the mailbox's own: only the suggested ones are added.
|
|
487
|
+
for (const contact of checkedContacts.filter((checked) => checked.folderUid === suggestedFolderUid)) {
|
|
432
488
|
try {
|
|
433
489
|
await createContact(
|
|
434
490
|
{
|
|
@@ -540,7 +596,7 @@ function ContactsContent({ userUid }: { userUid?: string }) {
|
|
|
540
596
|
onToggleFavorite={handleToggleFavorite}
|
|
541
597
|
onAddCategory={handleAddCategory}
|
|
542
598
|
onExportVCard={handleExportVCard}
|
|
543
|
-
onAddToContacts={view.type === "suggested" ? handleAddToContacts : undefined}
|
|
599
|
+
onAddToContacts={view.type === "suggested" || view.type === "everyone" ? handleAddToContacts : undefined}
|
|
544
600
|
onImportFile={handleImportFile}
|
|
545
601
|
shortcuts
|
|
546
602
|
hideNew={isMobile}
|
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">
|