@rapidmx/web-client 0.22.0 → 0.23.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/shared/components/admin/setup/SetupWizard.tsx +1 -1
- package/apps/shared/components/calendar/TimeGridView.tsx +3 -1
- package/apps/shared/components/mail/ConversationList.tsx +18 -5
- package/apps/shared/components/mail/ConversationThreadPane.tsx +7 -2
- package/apps/shared/components/mail/MessageDetailPane.tsx +37 -18
- package/apps/shared/components/mail/invite/InviteRowChip.tsx +8 -12
- package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +2 -2
- package/apps/shared/components/mail/reading/conversationPrint.ts +76 -0
- package/apps/shared/components/mail/reading/printMessage.ts +44 -7
- package/apps/shared/keyboard/keymap.ts +1 -0
- package/apps/shared/mail/decryptedMessages.ts +116 -0
- package/apps/www/index.tsx +11 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +4 -4
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +1 -1
- package/dist/apps/shared/components/calendar/TimeGridView.js +1 -1
- package/dist/apps/shared/components/mail/ConversationList.js +17 -4
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +6 -2
- package/dist/apps/shared/components/mail/MessageDetailPane.js +31 -18
- package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +2 -2
- package/dist/apps/shared/components/mail/invite/InviteRowChip.js +7 -9
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +1 -1
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +15 -0
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +69 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +7 -1
- package/dist/apps/shared/components/mail/reading/printMessage.js +33 -6
- package/dist/apps/shared/keyboard/keymap.d.ts +6 -0
- package/dist/apps/shared/keyboard/keymap.js +1 -0
- package/dist/apps/shared/mail/decryptedMessages.d.ts +27 -0
- package/dist/apps/shared/mail/decryptedMessages.js +97 -0
- package/dist/apps/www/index.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
import { getUnlockedKeys, subscribeKeySession } from "../../../lib/crypto/keySession.js";
|
|
7
|
+
import { getMessageRawContent } from "../../../lib/mail/mailApi.js";
|
|
8
|
+
const PREVIEW_MAX_LENGTH = 160;
|
|
9
|
+
let entries = {};
|
|
10
|
+
/** Bumped whenever a mailbox's keys are unlocked, so a view can retry decrypting what it couldn't before. */
|
|
11
|
+
let unlockEpoch = 0;
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const inFlight = new Set();
|
|
14
|
+
function emit() {
|
|
15
|
+
for (const listener of [...listeners]) {
|
|
16
|
+
listener();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
subscribeKeySession(({ mailboxUid, state }) => {
|
|
20
|
+
if (state === "locked") {
|
|
21
|
+
const kept = Object.fromEntries(Object.entries(entries).filter(([, entry]) => entry.mailboxUid !== mailboxUid));
|
|
22
|
+
if (Object.keys(kept).length !== Object.keys(entries).length) {
|
|
23
|
+
entries = kept;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
unlockEpoch++;
|
|
28
|
+
}
|
|
29
|
+
emit();
|
|
30
|
+
});
|
|
31
|
+
function subscribe(listener) {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(listener);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Plain text of decrypted HTML for a one-line preview. Only ever feeds text display, never markup. */
|
|
38
|
+
export function previewOf(html) {
|
|
39
|
+
return html
|
|
40
|
+
.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ")
|
|
41
|
+
.replace(/<[^>]+>/g, " ")
|
|
42
|
+
.replace(/ /gi, " ")
|
|
43
|
+
.replace(/&/gi, "&")
|
|
44
|
+
.replace(/</gi, "<")
|
|
45
|
+
.replace(/>/gi, ">")
|
|
46
|
+
.replace(/"/gi, '"')
|
|
47
|
+
.replace(/�*39;/gi, "'")
|
|
48
|
+
.replace(/\s+/g, " ")
|
|
49
|
+
.trim()
|
|
50
|
+
.slice(0, PREVIEW_MAX_LENGTH);
|
|
51
|
+
}
|
|
52
|
+
/** Remembers what was recovered of one message. Ignored when there is nothing new to show. */
|
|
53
|
+
export function rememberDecrypted(uid, mailboxUid, found) {
|
|
54
|
+
if (!found.subject && !found.preview) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const current = entries[uid];
|
|
58
|
+
if (current && current.subject === found.subject && current.preview === found.preview) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
entries = { ...entries, [uid]: { mailboxUid, subject: found.subject, preview: found.preview } };
|
|
62
|
+
emit();
|
|
63
|
+
}
|
|
64
|
+
/** Every message decrypted so far, by uid; a new object whenever any changes. */
|
|
65
|
+
export function useDecryptedMessages() {
|
|
66
|
+
return useSyncExternalStore(subscribe, () => entries);
|
|
67
|
+
}
|
|
68
|
+
/** A number that changes whenever a mailbox is unlocked - a dependency for effects that decrypt what they can. */
|
|
69
|
+
export function useUnlockEpoch() {
|
|
70
|
+
return useSyncExternalStore(subscribe, () => unlockEpoch);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decrypts one message to recover its subject and preview, when its mailbox's keys are unlocked and it isn't already known. Best effort: a
|
|
74
|
+
* message that can't be fetched or opened is simply left as it was. Never prompts.
|
|
75
|
+
*/
|
|
76
|
+
export async function decryptForDisplay(uid, mailboxUid) {
|
|
77
|
+
const unlocked = getUnlockedKeys(mailboxUid);
|
|
78
|
+
if (!unlocked || entries[uid] || inFlight.has(uid)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
inFlight.add(uid);
|
|
82
|
+
try {
|
|
83
|
+
const rawMime = await getMessageRawContent(uid);
|
|
84
|
+
const { evaluateMessageSecurity } = await import("../../../lib/crypto/messageSecurity.js");
|
|
85
|
+
const security = await evaluateMessageSecurity(rawMime, unlocked);
|
|
86
|
+
// A lock while this ran: the result must not go back on screen.
|
|
87
|
+
if (getUnlockedKeys(mailboxUid) === unlocked) {
|
|
88
|
+
rememberDecrypted(uid, mailboxUid, { subject: security.subject, preview: security.html ? previewOf(security.html) : security.text?.slice(0, PREVIEW_MAX_LENGTH) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Left as the placeholder.
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
inFlight.delete(uid);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/apps/www/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import { setReadStateMany } from "../shared/mail/messageReadState.js";
|
|
|
32
32
|
import { useMarkMessageRead } from "../shared/mail/useMarkMessageRead.js";
|
|
33
33
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "../shared/components/mail/unreadStyle.js";
|
|
34
34
|
import { LazyConversationThreadPane, LazyMessageDetailPane, prefetchReadingPane } from "../shared/components/mail/LazyReadingPane.js";
|
|
35
|
+
import { rememberDecrypted } from "../shared/mail/decryptedMessages.js";
|
|
35
36
|
import ConversationList, { conversationRowKey } from "../shared/components/mail/ConversationList.js";
|
|
36
37
|
import SwipeRow from "../shared/components/mail/SwipeRow.js";
|
|
37
38
|
import InviteRowChip from "../shared/components/mail/invite/InviteRowChip.js";
|
|
@@ -101,6 +102,7 @@ async function decryptEncryptedRows(messages, unlocked) {
|
|
|
101
102
|
return null;
|
|
102
103
|
}
|
|
103
104
|
const preview = security.html ? stripHtmlToText(security.html).slice(0, LIST_PREVIEW_MAX_LENGTH) : undefined;
|
|
105
|
+
rememberDecrypted(message.uid, message.mailboxUid, { subject: security.subject, preview });
|
|
104
106
|
return [message.uid, { subject: security.subject, preview }];
|
|
105
107
|
}
|
|
106
108
|
catch {
|
|
@@ -2076,11 +2078,19 @@ function InboxContent({ userUid }) {
|
|
|
2076
2078
|
}
|
|
2077
2079
|
await actOnKeyboardTargets(targets.filter((message) => !inDeletedItems(message)), (chosen) => moveToFolderOfType(chosen, "deleted_items", "Deleted Items"), true);
|
|
2078
2080
|
}
|
|
2081
|
+
/** Shift+Delete, from the keyboard: permanently deletes the targets wherever they are, after the same confirmation as Delete in Deleted Items. */
|
|
2082
|
+
async function permanentDeleteFromKeyboard() {
|
|
2083
|
+
const targets = await keyboardTargets();
|
|
2084
|
+
if (targets.length > 0 && (await purgeChosen(targets)) && threadPane && !selectMode) {
|
|
2085
|
+
closeOpenThread();
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2079
2088
|
useShortcut(SHORTCUTS.mail.next, () => selectNeighbour(1), { enabled: canMoveSelection });
|
|
2080
2089
|
useShortcut(SHORTCUTS.mail.previous, () => selectNeighbour(-1), { enabled: canMoveSelection });
|
|
2081
2090
|
useShortcut(SHORTCUTS.mail.nextUnread, () => selectNextUnread(1), { enabled: canMoveSelection });
|
|
2082
2091
|
useShortcut(SHORTCUTS.mail.previousUnread, () => selectNextUnread(-1), { enabled: canMoveSelection });
|
|
2083
2092
|
useShortcut(SHORTCUTS.mail.delete, () => void deleteFromKeyboard(), { enabled: keyboardActions && keyboardTargetExists });
|
|
2093
|
+
useShortcut(SHORTCUTS.mail.permanentDelete, () => void permanentDeleteFromKeyboard(), { enabled: keyboardActions && keyboardTargetExists });
|
|
2084
2094
|
useShortcut(SHORTCUTS.mail.markRead, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, true, { patch: patchListedMessage, track: trackMessageChange }), false, isUnread), { enabled: keyboardActions && keyboardTargetExists });
|
|
2085
2095
|
useShortcut(SHORTCUTS.mail.markUnread, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, false, { patch: patchListedMessage, track: trackMessageChange }), false, (message) => !isUnread(message)), { enabled: keyboardActions && keyboardTargetExists });
|
|
2086
2096
|
useShortcut(SHORTCUTS.mail.flag, () =>
|
package/package.json
CHANGED