@rapidmx/web-client 0.35.0 → 0.37.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/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +113 -0
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +89 -3
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +8 -4
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +30 -1
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +122 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
- package/apps/shared/components/mail/ConversationList.tsx +1 -1
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +59 -26
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/draftResume.ts +28 -3
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
- package/apps/shared/components/mail/invite/inviteStore.ts +17 -1
- package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
- package/apps/shared/components/settings/KeyExportSetting.tsx +173 -0
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +128 -0
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/decryptedMessages.ts +11 -3
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/mail/reportNotices.ts +3 -1
- package/apps/shared/mail/zeroAccessSource.ts +27 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +84 -24
- package/apps/shared/search/localIndexBuilder.ts +57 -20
- package/apps/shared/search/localIndexRpcClient.ts +96 -12
- package/apps/shared/search/localIndexSchema.ts +20 -4
- package/apps/shared/search/localIndexTransport.ts +6 -2
- package/apps/shared/search/localIndexWorker.ts +37 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +679 -0
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +225 -61
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +19 -3
- package/apps/www/settings/encryption/index.tsx +21 -1
- package/apps/www/settings/privacy/index.tsx +89 -39
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/escrow/matters/[uid].js +2 -2
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +14 -0
- package/dist/apps/shared/calendar/sealedEvents.js +85 -0
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +25 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +27 -2
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +17 -0
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +98 -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/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +46 -21
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- 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/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- 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 +16 -1
- package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- 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 +90 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -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 +16 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +80 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- 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.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- 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 +74 -24
- package/dist/apps/shared/search/localIndexBuilder.d.ts +9 -1
- package/dist/apps/shared/search/localIndexBuilder.js +50 -17
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +129 -11
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +18 -4
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -2
- package/dist/apps/shared/search/localIndexWorker.d.ts +11 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +102 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.js +544 -0
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +88 -20
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +19 -3
- package/dist/apps/www/settings/encryption/index.js +16 -4
- package/dist/apps/www/settings/privacy/index.js +47 -6
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
- package/dist/lib/calendar/calendarApi.d.ts +52 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +86 -7
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +9 -0
- package/dist/lib/calendar/sealedInvites.js +65 -0
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.d.ts +54 -0
- package/dist/lib/crypto/keyExport.js +161 -0
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +64 -3
- package/dist/lib/crypto/keySession.js +179 -12
- package/dist/lib/crypto/keyvaultApi.d.ts +129 -8
- package/dist/lib/crypto/keyvaultApi.js +45 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +56 -3
- package/dist/lib/crypto/messageSecurity.js +201 -13
- package/dist/lib/crypto/mime.d.ts +5 -0
- package/dist/lib/crypto/mime.js +25 -0
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +62 -0
- package/dist/lib/crypto/sharedMailboxKeys.js +337 -0
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
- package/dist/lib/crypto/zeroAccessKey.js +71 -0
- package/dist/lib/mail/conversationsApi.d.ts +6 -0
- package/dist/lib/mail/mailApi.d.ts +63 -2
- package/dist/lib/mail/mailApi.js +5 -1
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +85 -0
- package/dist/lib/search/zeroAccessIndexApi.js +35 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/admin/escrowAccessRequestsApi.ts +6 -0
- package/lib/calendar/calendarApi.ts +143 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +101 -8
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +73 -0
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +229 -0
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +216 -12
- package/lib/crypto/keyvaultApi.ts +174 -10
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +284 -16
- package/lib/crypto/mime.ts +26 -0
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +402 -0
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +102 -0
- package/lib/mail/conversationsApi.ts +3 -0
- package/lib/mail/mailApi.ts +60 -3
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +123 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +14 -5
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import { subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
7
|
+
/**
|
|
8
|
+
* Calls `onLocked` with the mailbox whenever a mailbox's keys lock (the idle timeout, a lock from settings, signing out) - for a page that
|
|
9
|
+
* holds records it opened with them in its state: the opened-value caches are cleared on a lock, but a page's state is not, so it reloads
|
|
10
|
+
* (the reloaded records keep their placeholders) and closes what shows opened content. The latest `onLocked` is called; subscribed once.
|
|
11
|
+
*/
|
|
12
|
+
export function useKeysLocked(onLocked) {
|
|
13
|
+
const latest = useRef(onLocked);
|
|
14
|
+
latest.current = onLocked;
|
|
15
|
+
useEffect(() => subscribeKeySession(({ mailboxUid, state }) => {
|
|
16
|
+
if (state === "locked") {
|
|
17
|
+
latest.current(mailboxUid);
|
|
18
|
+
}
|
|
19
|
+
}), []);
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SealedRecord } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
2
|
+
/**
|
|
3
|
+
* `record` with all its protected fields opened (`openSealedRecord()`), for a view that shows or edits every field - a list opens only
|
|
4
|
+
* what it shows. `ready` is false while a protected record is first being opened; `record` is the one given meanwhile, and the given one
|
|
5
|
+
* again when it can't be opened (the mailbox is locked).
|
|
6
|
+
*
|
|
7
|
+
* A new object for the same record (same `uid`) keeps the opened one ready while it is opened in turn, so a form over it isn't unmounted by
|
|
8
|
+
* a live update (its in-progress edits kept); the new one - its new `version` - takes over once opened. The record's mailbox locking drops
|
|
9
|
+
* what was opened at once (nothing opened outlives its key session) and opens it again, which gives the placeholders; its unlocking opens it
|
|
10
|
+
* again too.
|
|
11
|
+
*
|
|
12
|
+
* `open` opens it, `openSealedRecord()` unless a kind of record has its own full open (a calendar event's `fullyOpenedEvent()`, which keeps
|
|
13
|
+
* the title opened from its sealed content) - a function defined once, not per render, so the record isn't opened again on every render.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useOpenedRecord<T extends SealedRecord>(record: T | undefined, open?: (record: T) => Promise<T>): {
|
|
16
|
+
record: T | undefined;
|
|
17
|
+
ready: boolean;
|
|
18
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
7
|
+
import { isProtectedRecord, openSealedRecord } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
8
|
+
/** The record's `uid`, when it has one - a new object for the same record (a live update, the page's own reload) is the same record. */
|
|
9
|
+
function uidOf(record) {
|
|
10
|
+
return record?.uid;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `record` with all its protected fields opened (`openSealedRecord()`), for a view that shows or edits every field - a list opens only
|
|
14
|
+
* what it shows. `ready` is false while a protected record is first being opened; `record` is the one given meanwhile, and the given one
|
|
15
|
+
* again when it can't be opened (the mailbox is locked).
|
|
16
|
+
*
|
|
17
|
+
* A new object for the same record (same `uid`) keeps the opened one ready while it is opened in turn, so a form over it isn't unmounted by
|
|
18
|
+
* a live update (its in-progress edits kept); the new one - its new `version` - takes over once opened. The record's mailbox locking drops
|
|
19
|
+
* what was opened at once (nothing opened outlives its key session) and opens it again, which gives the placeholders; its unlocking opens it
|
|
20
|
+
* again too.
|
|
21
|
+
*
|
|
22
|
+
* `open` opens it, `openSealedRecord()` unless a kind of record has its own full open (a calendar event's `fullyOpenedEvent()`, which keeps
|
|
23
|
+
* the title opened from its sealed content) - a function defined once, not per render, so the record isn't opened again on every render.
|
|
24
|
+
*/
|
|
25
|
+
export function useOpenedRecord(record, open = openSealedRecord) {
|
|
26
|
+
const [opened, setOpened] = useState(undefined);
|
|
27
|
+
const [generation, setGeneration] = useState(0);
|
|
28
|
+
const protectedRecord = isProtectedRecord(record);
|
|
29
|
+
const mailboxUid = record?.mailboxUid;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!mailboxUid || !protectedRecord) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
return subscribeKeySession((event) => {
|
|
35
|
+
if (event.mailboxUid !== mailboxUid) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (event.state === "locked") {
|
|
39
|
+
setOpened(undefined);
|
|
40
|
+
}
|
|
41
|
+
setGeneration((current) => current + 1);
|
|
42
|
+
});
|
|
43
|
+
}, [mailboxUid, protectedRecord]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!record || !protectedRecord) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
void open(record).then((result) => {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setOpened({ from: record, record: result });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [record, protectedRecord, generation, open]);
|
|
58
|
+
if (!record || !protectedRecord) {
|
|
59
|
+
return { record, ready: true };
|
|
60
|
+
}
|
|
61
|
+
if (opened?.from === record) {
|
|
62
|
+
return { record: opened.record, ready: true };
|
|
63
|
+
}
|
|
64
|
+
const uid = uidOf(record);
|
|
65
|
+
return opened && uid !== undefined && uidOf(opened.from) === uid ? { record: opened.record, ready: true } : { record, ready: false };
|
|
66
|
+
}
|
|
@@ -23,4 +23,4 @@ export type WwwShellProps = PropsWithChildren<Omit<AppShellProps, "active">>;
|
|
|
23
23
|
* app. Anything else - Mail's `/` and `/messages/:uid` - is Mail.
|
|
24
24
|
*/
|
|
25
25
|
export declare function activeAppOf(route: string): AppShellActive;
|
|
26
|
-
export default function WwwShell({ children, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance, }: WwwShellProps): React.JSX.Element;
|
|
26
|
+
export default function WwwShell({ children, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance, signOutHost, }: WwwShellProps): React.JSX.Element;
|
package/dist/apps/www/_shell.js
CHANGED
|
@@ -16,7 +16,7 @@ export function activeAppOf(route) {
|
|
|
16
16
|
const app = route.split("/")[1];
|
|
17
17
|
return app === "calendar" || app === "contacts" || app === "tasks" || app === "settings" ? app : "mail";
|
|
18
18
|
}
|
|
19
|
-
export default function WwwShell({ children, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance, }) {
|
|
19
|
+
export default function WwwShell({ children, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance, signOutHost, }) {
|
|
20
20
|
const { route, pending } = useRouter();
|
|
21
21
|
const [takeovers, setTakeovers] = useState(0);
|
|
22
22
|
const enterTakeover = useCallback(() => {
|
|
@@ -27,5 +27,5 @@ export default function WwwShell({ children, userUid, authServerUrl, impersonati
|
|
|
27
27
|
// The content region is what a page load would have started at: focus it (it is only focusable by script), start at the top, and say the
|
|
28
28
|
// page's title - without the unread count that the tab's title carries in front of it.
|
|
29
29
|
useNavigationEffects({ focus: "#app-content", scroll: "top", announce: ({ title }) => titleWithUnread(title, 0) });
|
|
30
|
-
return (_jsx(AppFrameContext.Provider, { value: frame, children: _jsx(AppChrome, { active: activeAppOf(route), busy: pending, hideChrome: takeovers > 0, userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, branding: branding, appearance: appearance, children: children }) }));
|
|
30
|
+
return (_jsx(AppFrameContext.Provider, { value: frame, children: _jsx(AppChrome, { active: activeAppOf(route), busy: pending, hideChrome: takeovers > 0, userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, branding: branding, appearance: appearance, signOutHost: signOutHost, children: children }) }));
|
|
31
31
|
}
|
|
@@ -12,16 +12,20 @@ import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth,
|
|
|
12
12
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
13
13
|
import { useApiClient } from "../../../lib/util/apiClientContext.js";
|
|
14
14
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
15
|
-
import { getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
15
|
+
import { fullyOpenedEvent, getCalendarEvent, listCalendarEvents } from "../../../lib/calendar/calendarApi.js";
|
|
16
|
+
import { isOpenedRecord, isProtectedRecord } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
17
|
+
import { isSealedRecord } from "../../../lib/zeroAccess/sealedMoves.js";
|
|
18
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
19
|
+
import { openSealedEvent, openSealedEvents } from "../../shared/calendar/sealedEvents.js";
|
|
16
20
|
import { useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
17
|
-
import { moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
21
|
+
import { SealedOccurrenceError, moveOccurrence, resizeOccurrenceEnd } from "../../../lib/calendar/calendarMutations.js";
|
|
18
22
|
import { resolveDragAction } from "../../../lib/calendar/calendarDragIds.js";
|
|
19
23
|
import { createFolder } from "../../../lib/mail/mailApi.js";
|
|
20
24
|
import { expandAllOccurrences, expandOccurrences } from "../../../lib/calendar/recurrence.js";
|
|
21
25
|
import { useDayKey } from "../../../lib/calendar/useDayKey.js";
|
|
22
|
-
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex } from "../../../lib/calendar/eventSearch.js";
|
|
26
|
+
import { allOccurrences, initialMatchIndex, occurrencesInRange, searchOccurrences, searchTerms, stepSequenceIndex, } from "../../../lib/calendar/eventSearch.js";
|
|
23
27
|
import Drawer from "../../../lib/components/overlays/Drawer.js";
|
|
24
|
-
import CalendarShell, { useCalendarShell } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
28
|
+
import CalendarShell, { useCalendarShell, } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
25
29
|
import CalendarListSidebar from "../../shared/components/calendar/CalendarListSidebar.js";
|
|
26
30
|
import CalendarSearchBox from "../../shared/components/calendar/CalendarSearchBox.js";
|
|
27
31
|
import { ActiveOccurrenceContext } from "../../shared/components/calendar/activeOccurrence.js";
|
|
@@ -37,7 +41,7 @@ import SplitDayView from "../../shared/components/calendar/SplitDayView.js";
|
|
|
37
41
|
import TimeGridView from "../../shared/components/calendar/TimeGridView.js";
|
|
38
42
|
import Alert from "../../../lib/components/feedback/Alert.js";
|
|
39
43
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
40
|
-
import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView } from "../../shared/components/calendar/calendarView.js";
|
|
44
|
+
import { CALENDAR_VIEWS, getStoredCalendarView, isCalendarView, storeCalendarView, } from "../../shared/components/calendar/calendarView.js";
|
|
41
45
|
import { SWIPE_PERIOD_SHIFT } from "../../shared/components/calendar/swipeNavigation.js";
|
|
42
46
|
import { nextHalfHour } from "../../shared/components/calendar/timePicker.js";
|
|
43
47
|
import { useSwipeSlide } from "../../shared/gestures/useSwipeSlide.js";
|
|
@@ -47,6 +51,8 @@ import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
|
47
51
|
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
48
52
|
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
49
53
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
54
|
+
import { notify } from "../../shared/notifications/store.js";
|
|
55
|
+
import { KeysLockedError } from "../../../lib/crypto/masterKey.js";
|
|
50
56
|
import { bookingSettingsHref } from "../../shared/calendar/bookingPlugin.js";
|
|
51
57
|
import { changedEventOf } from "../../shared/calendar/calendarLiveUpdates.js";
|
|
52
58
|
/** The push client channel group (`PushClient.setChannels()`) the page's calendars are subscribed under. */
|
|
@@ -56,7 +62,14 @@ const MAX_DATE_MS = 8.64e15;
|
|
|
56
62
|
function CalendarPage(props) {
|
|
57
63
|
return (_jsx(CalendarShell, { ...props, children: _jsx(CalendarContent, { userUid: props.userUid, bookingHref: bookingSettingsHref(props.pluginNav) }) }));
|
|
58
64
|
}
|
|
59
|
-
const VIEW_LABELS = {
|
|
65
|
+
const VIEW_LABELS = {
|
|
66
|
+
list: "List",
|
|
67
|
+
month: "Month",
|
|
68
|
+
week: "Week",
|
|
69
|
+
workWeek: "Work Week",
|
|
70
|
+
day: "Day",
|
|
71
|
+
split: "Split",
|
|
72
|
+
};
|
|
60
73
|
/** The shortcut that switches to a view (Outlook's Ctrl+Alt+1-4, the List one more); the split view has none. */
|
|
61
74
|
const VIEW_SHORTCUTS = {
|
|
62
75
|
list: SHORTCUTS.calendar.list,
|
|
@@ -66,7 +79,7 @@ const VIEW_SHORTCUTS = {
|
|
|
66
79
|
month: SHORTCUTS.calendar.month,
|
|
67
80
|
};
|
|
68
81
|
/** One of the view switcher's buttons, with its shortcut in the tooltip. */
|
|
69
|
-
function ViewButton({ view, current, onSelect }) {
|
|
82
|
+
function ViewButton({ view, current, onSelect, }) {
|
|
70
83
|
const shortcut = VIEW_SHORTCUTS[view];
|
|
71
84
|
const hint = useShortcutProps(VIEW_LABELS[view], shortcut ?? SHORTCUTS.calendar.day, !!shortcut);
|
|
72
85
|
return (_jsx("button", { type: "button", onClick: () => onSelect(view), ...hint, className: [
|
|
@@ -198,7 +211,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
198
211
|
}
|
|
199
212
|
setLoading(true);
|
|
200
213
|
setError(null);
|
|
201
|
-
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client)))
|
|
214
|
+
void Promise.allSettled(targets.map((f) => listCalendarEvents(f.uid, client).then(openSealedEvents)))
|
|
202
215
|
.then((results) => {
|
|
203
216
|
if (seq !== reloadSeqRef.current) {
|
|
204
217
|
return;
|
|
@@ -237,7 +250,10 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
237
250
|
}
|
|
238
251
|
// Keyed on stable string summaries (not the array/set objects themselves, which are new references
|
|
239
252
|
// every render) so this only re-fires when the actual set of folders to fetch changes.
|
|
240
|
-
const calendarFolderUidsKey = calendarFolders
|
|
253
|
+
const calendarFolderUidsKey = calendarFolders
|
|
254
|
+
.map((f) => f.uid)
|
|
255
|
+
.sort()
|
|
256
|
+
.join(",");
|
|
241
257
|
const checkedFolderUidsKey = Array.from(checkedFolderUids).sort().join(",");
|
|
242
258
|
useEffect(reload, [calendarFolderUidsKey, checkedFolderUidsKey]);
|
|
243
259
|
// An event that somebody creates, changes or deletes is announced on the push connection - a private or confidential one as its busy block, whatever the
|
|
@@ -254,7 +270,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
254
270
|
setEvents((previous) => previous.filter((existing) => existing.uid !== uid));
|
|
255
271
|
return;
|
|
256
272
|
}
|
|
257
|
-
void getCalendarEvent(uid, client)
|
|
273
|
+
void getCalendarEvent(uid, client)
|
|
274
|
+
.then(openSealedEvent)
|
|
275
|
+
.then((fresh) => {
|
|
258
276
|
if (checkedFolderUidsRef.current.has(fresh.folderUid)) {
|
|
259
277
|
setEvents((previous) => previous.some((existing) => existing.uid === fresh.uid)
|
|
260
278
|
? previous.map((existing) => (existing.uid === fresh.uid ? fresh : existing))
|
|
@@ -282,11 +300,41 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
282
300
|
// Today moves on at midnight: the List's "from today on" is worked out again then (`listDay` is only a dependency).
|
|
283
301
|
const listDay = useDayKey();
|
|
284
302
|
const dayOfList = view === "list" ? listDay : "";
|
|
303
|
+
// A search matches descriptions too, which a list leaves sealed (`EVENT_LIST_FIELDS`): the protected events are opened in full once one
|
|
304
|
+
// runs, and searched as they are until then.
|
|
305
|
+
const [searchable, setSearchable] = useState(null);
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!searching || !events.some((event) => isProtectedRecord(event) && !isOpenedRecord(event))) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let cancelled = false;
|
|
311
|
+
void Promise.all(events.map((event) => fullyOpenedEvent(event))).then((opened) => {
|
|
312
|
+
if (!cancelled) {
|
|
313
|
+
setSearchable({ from: events, events: opened });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
return () => {
|
|
317
|
+
cancelled = true;
|
|
318
|
+
};
|
|
319
|
+
}, [events, searching]);
|
|
320
|
+
const searchedEvents = searchable?.from === events ? searchable.events : events;
|
|
321
|
+
// The calendar holds protected events opened with their mailboxes' keys: a lock of one takes them off screen - read again with their
|
|
322
|
+
// placeholders, the search's fully opened copies dropped, and an open card or form of a protected event closed - its own mailbox's
|
|
323
|
+
// lock closes it too when it isn't one of the listed events (opened from a reminder, its calendar unchecked).
|
|
324
|
+
useKeysLocked((locked) => {
|
|
325
|
+
const listed = events.some((event) => event.mailboxUid === locked);
|
|
326
|
+
setModal((current) => current?.occurrence && isSealedRecord(current.occurrence) && (listed || current.occurrence.mailboxUid === locked) ? null : current);
|
|
327
|
+
if (!listed) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
reload();
|
|
331
|
+
setSearchable(null);
|
|
332
|
+
});
|
|
285
333
|
const matches = useMemo(() => {
|
|
286
|
-
const found = searching ? searchOccurrences(
|
|
334
|
+
const found = searching ? searchOccurrences(searchedEvents, query) : [];
|
|
287
335
|
const today = new Date();
|
|
288
336
|
return view === "list" ? found.filter((occurrence) => isUpcoming(occurrence, today)) : found;
|
|
289
|
-
}, [
|
|
337
|
+
}, [searchedEvents, query, searching, view, dayOfList]);
|
|
290
338
|
const activeIndex = matches.findIndex((m) => m.occurrenceKey === activeKey);
|
|
291
339
|
// 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
|
|
292
340
|
// 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.
|
|
@@ -305,7 +353,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
305
353
|
if (view === "list") {
|
|
306
354
|
return sequence;
|
|
307
355
|
}
|
|
308
|
-
return searching
|
|
356
|
+
return searching
|
|
357
|
+
? occurrencesInRange(matches, rangeStart, rangeEnd)
|
|
358
|
+
: expandAllOccurrences(events, rangeStart, rangeEnd);
|
|
309
359
|
}, [events, matches, searching, view, sequence, rangeStart, rangeEnd]);
|
|
310
360
|
/** Points at `match`, moving the view to the day it is on unless the view already shows it. */
|
|
311
361
|
function showMatch(match) {
|
|
@@ -354,7 +404,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
354
404
|
searchedQueryRef.current = query;
|
|
355
405
|
// The List holds every match: it starts at the first from the day being viewed on rather than the first of all.
|
|
356
406
|
const from = startOfDay(viewDate);
|
|
357
|
-
showMatch(matches[view === "list"
|
|
407
|
+
showMatch(matches[view === "list"
|
|
408
|
+
? initialMatchIndex(matches, from, rangeEnd, from)
|
|
409
|
+
: initialMatchIndex(matches, rangeStart, rangeEnd, from)]);
|
|
358
410
|
}
|
|
359
411
|
}, [matches, query, searching]);
|
|
360
412
|
const splitColumns = useMemo(() => calendarFolders
|
|
@@ -407,7 +459,9 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
407
459
|
// Brings the match being pointed at into sight (the views scroll their own hours; the page may too).
|
|
408
460
|
useEffect(() => {
|
|
409
461
|
const marked = Array.from(swipe.ref.current.querySelectorAll("[data-occurrence-key]"));
|
|
410
|
-
marked
|
|
462
|
+
marked
|
|
463
|
+
.find((el) => el.dataset.occurrenceKey === activeKey)
|
|
464
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
411
465
|
}, [activeKey, view === "list" ? null : viewDate, view, loading]);
|
|
412
466
|
// 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.
|
|
413
467
|
const [jumpNonce, setJumpNonce] = useState(0);
|
|
@@ -461,10 +515,12 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
461
515
|
const requestedStart = new Date(params.get("start") ?? "");
|
|
462
516
|
navigateTo("/calendar", { replace: true });
|
|
463
517
|
getCalendarEvent(eventUid, client)
|
|
518
|
+
.then(openSealedEvent)
|
|
464
519
|
.then((event) => {
|
|
465
520
|
const from = isNaN(requestedStart.getTime()) ? new Date(event.startDate) : requestedStart;
|
|
466
521
|
const candidates = expandOccurrences(event, new Date(from.getTime() - 86400000), new Date(from.getTime() + 86400000));
|
|
467
|
-
const match = candidates.find((occurrence) => new Date(occurrence.startDate).getTime() === from.getTime()) ??
|
|
522
|
+
const match = candidates.find((occurrence) => new Date(occurrence.startDate).getTime() === from.getTime()) ??
|
|
523
|
+
candidates[0];
|
|
468
524
|
if (match) {
|
|
469
525
|
setViewDate(occurrenceDay(match));
|
|
470
526
|
openEvent(match);
|
|
@@ -515,7 +571,14 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
515
571
|
reload();
|
|
516
572
|
}
|
|
517
573
|
catch (err) {
|
|
518
|
-
|
|
574
|
+
const context = action.type === "move" ? "Couldn't move the event" : "Couldn't resize the event";
|
|
575
|
+
if (err instanceof KeysLockedError || err instanceof SealedOccurrenceError) {
|
|
576
|
+
// A protected occurrence while the mailbox is locked, or one of a zero-access invitation: nothing was written.
|
|
577
|
+
notify({ kind: "warning", title: context, message: err.message });
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
notifyApiError(err, context);
|
|
581
|
+
}
|
|
519
582
|
// A refused change (a 409 most of all) means the page holds a stale version: load again, or every later drag of this event fails the same way.
|
|
520
583
|
reload();
|
|
521
584
|
}
|
|
@@ -535,12 +598,17 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
535
598
|
const writableMailboxes = useWritableMailboxes(mailboxes, userUid, modalMailboxUid);
|
|
536
599
|
const mailboxOptions = useMemo(() => mailboxCalendars
|
|
537
600
|
.filter((mc) => mc.calendarFolders.length > 0 && writableMailboxes.some((mb) => mb.uid === mc.mailbox.uid))
|
|
538
|
-
.map((mc) => ({
|
|
601
|
+
.map((mc) => ({
|
|
602
|
+
mailbox: mc.mailbox,
|
|
603
|
+
calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })),
|
|
604
|
+
})), [mailboxCalendars, writableMailboxes]);
|
|
539
605
|
const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => {
|
|
540
606
|
setViewDate(startOfDay(date));
|
|
541
607
|
setJumpNonce((n) => n + 1);
|
|
542
608
|
} }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
|
|
543
|
-
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders
|
|
609
|
+
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx(CalendarSearchBox, { value: searchText, onChange: setSearchText, onSubmit: submitSearch, onClear: clearSearch, onPrevious: () => stepMatch(-1), onNext: () => stepMatch(1), active: searching, count: matches.length, position: activeIndex, inputRef: searchInputRef }), _jsx("div", { className: "flex flex-wrap gap-1", children: CALENDAR_VIEWS.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(ActiveOccurrenceContext.Provider, { value: searching ? activeKey : null, children: _jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "list" ? (_jsx(ListView, { occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, focusDate: viewDate, jumpNonce: jumpNonce, step: listStep })) : view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) }) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders
|
|
610
|
+
.filter((f) => f.mailboxUid === modalMailbox.uid)
|
|
611
|
+
.map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, matchNav: modal.occurrence && sequence.length > 0
|
|
544
612
|
? {
|
|
545
613
|
onPrevious: () => stepMatch(-1),
|
|
546
614
|
onNext: () => stepMatch(1),
|
|
@@ -549,7 +617,7 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
549
617
|
noun: searching ? "match" : "event",
|
|
550
618
|
slide,
|
|
551
619
|
}
|
|
552
|
-
: undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
|
|
620
|
+
: undefined, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && (_jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() }))] })] }));
|
|
553
621
|
}
|
|
554
622
|
export default CalendarPage;
|
|
555
623
|
/** The tab's title: `Brand: Calendar` (see `pageTitle()`). */
|
|
@@ -7,7 +7,7 @@ import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
|
7
7
|
import { useNavigate } from "../../shared/navigation/index.js";
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { ApiRequestError } from "../../../lib/util/api.js";
|
|
10
|
-
import { createContact, deleteContact, getContact, listContacts, ensureSuggestedContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
|
|
10
|
+
import { createContact, deleteContact, getContact, CONTACT_LIST_FIELDS, CONTACT_MATCH_FIELDS, listContacts, ensureSuggestedContacts, listContactsInFolders, listDeletedContacts, setContactFavorite, updateContact, } from "../../../lib/contacts/contactsApi.js";
|
|
11
11
|
import { VCARD_MAX_CHARS, VCARD_MAX_CONTACTS, contactsToVCardFile, contactToVCard, parseVCards } from "../../../lib/contacts/vcard.js";
|
|
12
12
|
import { withoutDuplicates } from "../../shared/components/contacts/participantDetails.js";
|
|
13
13
|
import useIsMobile from "../../../lib/util/useIsMobile.js";
|
|
@@ -34,6 +34,8 @@ import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
|
34
34
|
import { notifyApiError } from "../../shared/notifications/apiErrors.js";
|
|
35
35
|
import { notify } from "../../shared/notifications/store.js";
|
|
36
36
|
import { changedItemOf, useFolderLiveUpdates } from "../../shared/live/useFolderLiveUpdates.js";
|
|
37
|
+
import { isOpenedRecord, isProtectedRecord, openSealedRecord, openSealedRecords } from "../../../lib/zeroAccess/sealedRecords.js";
|
|
38
|
+
import { useKeysLocked } from "../../shared/zeroAccess/useKeysLocked.js";
|
|
37
39
|
/** The published model class name of a contact on either database (`ContactMongo`, `ContactSQL`) - not a `ContactList`'s. */
|
|
38
40
|
const CONTACT_MODEL = /^Contact(Mongo|SQL)$/;
|
|
39
41
|
/** The push client channel group (`PushClient.setChannels()`) the page's contacts folders are subscribed under. */
|
|
@@ -113,6 +115,7 @@ function ContactsContent({ userUid }) {
|
|
|
113
115
|
void loadSuggested(suggestedFolderUid);
|
|
114
116
|
}
|
|
115
117
|
return listAllPages((page) => listContacts(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
118
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items, CONTACT_LIST_FIELDS) }))
|
|
116
119
|
.then((result) => {
|
|
117
120
|
setContacts(result.items);
|
|
118
121
|
setTruncated(result.truncated);
|
|
@@ -123,10 +126,24 @@ function ContactsContent({ userUid }) {
|
|
|
123
126
|
useEffect(() => {
|
|
124
127
|
void reload();
|
|
125
128
|
}, [folderUid]);
|
|
129
|
+
// The lists hold protected contacts opened with the mailbox's keys: its lock takes them off screen, read again with their placeholders (the
|
|
130
|
+
// contacts, the suggested ones and the Deleted view). They leave the lists first, so a reload that fails can't leave them shown. An open
|
|
131
|
+
// contact's pane and form drop what they opened themselves (`useOpenedRecord`).
|
|
132
|
+
const [lockCount, setLockCount] = useState(0);
|
|
133
|
+
useKeysLocked((locked) => {
|
|
134
|
+
if (locked === mailboxUid) {
|
|
135
|
+
const readable = (list) => list.filter((contact) => !isProtectedRecord(contact));
|
|
136
|
+
setContacts(readable);
|
|
137
|
+
setSuggestedContacts(readable);
|
|
138
|
+
setDeletedContacts(readable);
|
|
139
|
+
void reload();
|
|
140
|
+
setLockCount((count) => count + 1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
126
143
|
/** Reads the Suggested Contacts folder's contacts. A failure leaves the list as it was. */
|
|
127
144
|
function loadSuggested(suggestedUid) {
|
|
128
145
|
return listAllPages((page) => listContacts(suggestedUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
129
|
-
.then((result) => setSuggestedContacts(result.items))
|
|
146
|
+
.then(async (result) => setSuggestedContacts(await openSealedRecords(result.items, CONTACT_LIST_FIELDS)))
|
|
130
147
|
.catch(() => undefined);
|
|
131
148
|
}
|
|
132
149
|
// Asks the server to turn the people this mailbox has corresponded with into suggested contacts (it does so once for each person, so this is cheap when
|
|
@@ -158,7 +175,9 @@ function ContactsContent({ userUid }) {
|
|
|
158
175
|
remove(change.uid);
|
|
159
176
|
return;
|
|
160
177
|
}
|
|
161
|
-
void getContact(change.uid, client)
|
|
178
|
+
void getContact(change.uid, client)
|
|
179
|
+
.then((fresh) => openSealedRecord(fresh, CONTACT_LIST_FIELDS))
|
|
180
|
+
.then((fresh) => {
|
|
162
181
|
if (fresh.deleted === true) {
|
|
163
182
|
remove(fresh.uid);
|
|
164
183
|
}
|
|
@@ -213,6 +232,7 @@ function ContactsContent({ userUid }) {
|
|
|
213
232
|
setDeletedLoading(true);
|
|
214
233
|
setDeletedError(null);
|
|
215
234
|
listAllPages((page) => listDeletedContacts(folderUid, { limit: LIST_PAGE_SIZE, page }, client))
|
|
235
|
+
.then(async (result) => ({ ...result, items: await openSealedRecords(result.items, CONTACT_LIST_FIELDS) }))
|
|
216
236
|
.then((result) => {
|
|
217
237
|
// A server that dropped the `deleted` filter (no delete+update right on this folder) answers with live
|
|
218
238
|
// contacts, which must never be listed as deleted.
|
|
@@ -228,8 +248,8 @@ function ContactsContent({ userUid }) {
|
|
|
228
248
|
.catch((err) => setDeletedError(err instanceof ApiRequestError ? err.message : "Could not load deleted contacts."))
|
|
229
249
|
.finally(() => setDeletedLoading(false));
|
|
230
250
|
// Re-fetches every time the Deleted view is (re-)selected, matching this page's own `reload()`
|
|
231
|
-
// convention elsewhere rather than caching a possibly-stale list across visits.
|
|
232
|
-
}, [view, folderUid]);
|
|
251
|
+
// convention elsewhere rather than caching a possibly-stale list across visits - and when the mailbox locks.
|
|
252
|
+
}, [view, folderUid, lockCount]);
|
|
233
253
|
const viewFiltered = useMemo(() => {
|
|
234
254
|
switch (view.type) {
|
|
235
255
|
case "all":
|
|
@@ -406,9 +426,17 @@ function ContactsContent({ userUid }) {
|
|
|
406
426
|
if (!category?.trim()) {
|
|
407
427
|
return;
|
|
408
428
|
}
|
|
409
|
-
|
|
410
|
-
|
|
429
|
+
let locked = 0;
|
|
430
|
+
for (const listed of checkedContacts) {
|
|
411
431
|
try {
|
|
432
|
+
// A protected one's categories are added to as opened: one that won't open here (the mailbox is locked) shows a placeholder
|
|
433
|
+
// (none), and saving that plus the new one would replace its real categories for good. It is left as it is.
|
|
434
|
+
const contact = await openSealedRecord(listed);
|
|
435
|
+
if (isProtectedRecord(contact) && !isOpenedRecord(contact)) {
|
|
436
|
+
locked++;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const categories = Array.from(new Set([...(contact.categories ?? []), category.trim()]));
|
|
412
440
|
// Only the changed field - sending the whole fetched contact back includes server-managed
|
|
413
441
|
// fields restapi rejects, and would overwrite any concurrent edit to the other fields.
|
|
414
442
|
await updateContact({ uid: contact.uid, version: contact.version, categories }, client);
|
|
@@ -417,14 +445,30 @@ function ContactsContent({ userUid }) {
|
|
|
417
445
|
notifyApiError(err, "Couldn't update some of the contacts");
|
|
418
446
|
}
|
|
419
447
|
}
|
|
448
|
+
if (locked > 0) {
|
|
449
|
+
notify({
|
|
450
|
+
kind: "warning",
|
|
451
|
+
title: "Some contacts weren't categorized",
|
|
452
|
+
message: `${locked === 1 ? "A contact is" : `${locked} contacts are`} protected and can't be changed while your mailbox is locked. Unlock it, then add the category again.`,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
420
455
|
clearPinnedSignerCache();
|
|
421
456
|
await reload();
|
|
422
457
|
});
|
|
423
458
|
// 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).
|
|
424
459
|
const handleAddToContacts = singleFlight(async () => {
|
|
425
460
|
// In the All contacts view the selection can hold contacts that already are the mailbox's own: only the suggested ones are added.
|
|
426
|
-
|
|
461
|
+
let locked = 0;
|
|
462
|
+
for (const listed of checkedContacts.filter((checked) => checked.folderUid === suggestedFolderUid)) {
|
|
427
463
|
try {
|
|
464
|
+
// Copied in full: the list opened only what it shows.
|
|
465
|
+
const contact = await openSealedRecord(listed);
|
|
466
|
+
// A protected one that won't open here (the mailbox is locked) holds only placeholders: copying them would replace its real
|
|
467
|
+
// name for good once the original is deleted. It is left as it is.
|
|
468
|
+
if (isProtectedRecord(contact) && !isOpenedRecord(contact)) {
|
|
469
|
+
locked++;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
428
472
|
await createContact({
|
|
429
473
|
mailboxUid: contact.mailboxUid,
|
|
430
474
|
folderUid: folderUid,
|
|
@@ -445,11 +489,20 @@ function ContactsContent({ userUid }) {
|
|
|
445
489
|
notifyApiError(err, "Couldn't add some of the contacts");
|
|
446
490
|
}
|
|
447
491
|
}
|
|
492
|
+
if (locked > 0) {
|
|
493
|
+
notify({
|
|
494
|
+
kind: "warning",
|
|
495
|
+
title: "Some contacts weren't added",
|
|
496
|
+
message: `${locked === 1 ? "A suggested contact is" : `${locked} suggested contacts are`} protected and can't be opened while your mailbox is locked. Unlock it, then add ${locked === 1 ? "it" : "them"} again.`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
448
499
|
setCheckedUids(new Set());
|
|
449
500
|
await reload();
|
|
450
501
|
});
|
|
451
|
-
function handleExportVCard() {
|
|
452
|
-
|
|
502
|
+
async function handleExportVCard() {
|
|
503
|
+
// Exported in full: the list opened only what it shows.
|
|
504
|
+
const contacts = await openSealedRecords(checkedContacts);
|
|
505
|
+
downloadTextFile(contacts.length === 1 ? `${contacts[0].displayName}.vcf` : "contacts.vcf", contacts.length === 1 ? contactToVCard(contacts[0]) : contactsToVCardFile(contacts));
|
|
453
506
|
}
|
|
454
507
|
async function handleImportFile(file) {
|
|
455
508
|
if (!mailboxUid || !folderUid) {
|
|
@@ -476,7 +529,7 @@ function ContactsContent({ userUid }) {
|
|
|
476
529
|
// earlier card's in the file - is left alone. The folder is read in full for that, not just the page of it the list holds.
|
|
477
530
|
let fresh;
|
|
478
531
|
try {
|
|
479
|
-
fresh = withoutDuplicates(parsed, await listContactsInFolders([folderUid], client)).fresh;
|
|
532
|
+
fresh = withoutDuplicates(parsed, await openSealedRecords(await listContactsInFolders([folderUid], client), CONTACT_MATCH_FIELDS)).fresh;
|
|
480
533
|
}
|
|
481
534
|
catch (err) {
|
|
482
535
|
notifyApiError(err, "Couldn't check the file against your contacts");
|
package/dist/apps/www/index.js
CHANGED
|
@@ -99,12 +99,28 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
99
99
|
let securityModule;
|
|
100
100
|
// A few at a time: each is a download and a public-key decryption.
|
|
101
101
|
const entries = await mapWithConcurrency(encrypted, DECRYPT_CONCURRENCY, async (message) => {
|
|
102
|
+
// A lock while a row was opened has already cleared the shared store: what it opened must not go back in (`decryptForDisplay()`
|
|
103
|
+
// asks the same). The caller's lock generation keeps it off this page.
|
|
104
|
+
const stillUnlocked = () => getUnlockedKeys(message.mailboxUid) === unlocked;
|
|
102
105
|
try {
|
|
103
|
-
const rawMime = await getMessageRawContent(message.uid);
|
|
104
106
|
// Loaded here, on first use: the S/MIME code (PKI.js, ASN.1, X.509) is over half a megabyte and an inbox with
|
|
105
107
|
// no encrypted mail in it never needs it.
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
+
const securityApi = await (securityModule ?? (securityModule = import("../../lib/crypto/messageSecurity.js")));
|
|
109
|
+
// A message sealed at rest carries its subject and preview sealed on the row: no download needed.
|
|
110
|
+
const summary = message.zeroAccess ? await securityApi.openZeroAccessSummary(message.zeroAccess.sealedSummary, unlocked.zeroAccessKeys) : undefined;
|
|
111
|
+
if (summary) {
|
|
112
|
+
const preview = summary.preview.slice(0, LIST_PREVIEW_MAX_LENGTH);
|
|
113
|
+
if (stillUnlocked()) {
|
|
114
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: summary.subject, preview });
|
|
115
|
+
}
|
|
116
|
+
return [message.uid, { subject: summary.subject, preview }];
|
|
117
|
+
}
|
|
118
|
+
const rawMime = await getMessageRawContent(message.uid);
|
|
119
|
+
const security = await securityApi.evaluateMessageSecurity(rawMime, unlocked);
|
|
120
|
+
// Nor is a lock a failure to decrypt.
|
|
121
|
+
if (!stillUnlocked()) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
108
124
|
if (!security.subject && !security.html) {
|
|
109
125
|
markDecryptFailed(message.uid);
|
|
110
126
|
return null;
|