@rapidmx/web-client 0.18.0 → 0.20.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/README.md +381 -381
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -482
- package/apps/admin/domains/[uid].tsx +166 -166
- package/apps/admin/escrow-scopes/[uid].tsx +350 -350
- package/apps/admin/index.tsx +129 -129
- package/apps/admin/mailboxes/[uid].tsx +271 -271
- package/apps/admin/mailboxes/new/index.tsx +30 -30
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/diagnostics/HostCard.tsx +2 -0
- package/apps/shared/components/admin/diagnostics/PressureTiles.tsx +108 -0
- package/apps/shared/components/admin/diagnostics/PvcTable.tsx +20 -6
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +21 -2
- package/apps/shared/components/admin/layout/AdminShell.tsx +384 -384
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -238
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -194
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +2093 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/settings/pluginPreferences.ts +34 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -446
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -103
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -461
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -439
- package/apps/shared/components/mail/ConversationList.tsx +332 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -613
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -240
- package/apps/shared/components/mail/MenuButton.tsx +404 -404
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1757
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1681
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/listPreferences.ts +3 -3
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -258
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -79
- package/apps/shared/components/mail/reading/messageExport.ts +59 -59
- package/apps/shared/components/mail/reading/printMessage.ts +99 -99
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -443
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderOfType.ts +49 -49
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/sendJob.ts +445 -445
- package/apps/shared/mail/outbox/sendOutcomes.ts +155 -155
- package/apps/shared/mail/reportNotices.ts +66 -66
- package/apps/shared/mail/senderBlocking.ts +141 -141
- package/apps/shared/mail/useMailConnection.ts +205 -205
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -60
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -396
- package/apps/www/calendar/index.tsx +581 -581
- package/apps/www/contacts/[uid].tsx +112 -112
- package/apps/www/index.tsx +3012 -2962
- package/apps/www/messages/[uid].tsx +139 -139
- package/apps/www/settings/auto-reply/index.tsx +136 -136
- package/apps/www/settings/blocked-senders/index.tsx +303 -303
- package/apps/www/settings/encryption/index.tsx +1290 -1290
- package/apps/www/settings/filters/[uid].tsx +179 -179
- package/apps/www/settings/filters/index.tsx +105 -105
- package/apps/www/settings/filters/new/index.tsx +165 -165
- package/apps/www/settings/labels/index.tsx +207 -207
- package/apps/www/settings/privacy/index.tsx +495 -495
- package/apps/www/settings/profile/index.tsx +251 -251
- package/apps/www/settings/read-receipts/index.tsx +150 -150
- package/apps/www/settings/sharing/index.tsx +259 -259
- package/apps/www/settings/signatures/[uid].tsx +175 -175
- package/apps/www/settings/signatures/index.tsx +91 -91
- package/apps/www/settings/signatures/new/index.tsx +138 -138
- package/apps/www/tasks/index.tsx +654 -654
- package/dist/apps/shared/components/admin/diagnostics/HostCard.js +2 -1
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.d.ts +22 -0
- package/dist/apps/shared/components/admin/diagnostics/PressureTiles.js +52 -0
- package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +9 -4
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +35 -2
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +328 -48
- package/dist/apps/shared/components/admin/settings/pluginPreferences.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/pluginPreferences.js +35 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +10 -3
- package/dist/apps/shared/components/mail/ConversationList.js +9 -5
- package/dist/apps/shared/components/mail/listPreferences.d.ts +1 -1
- package/dist/apps/shared/components/mail/reading/printMessage.js +11 -11
- package/dist/apps/shared/styles/app.css +396 -396
- package/dist/apps/www/index.js +51 -10
- package/package.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* Whether the Plugins page offers prerelease versions (`1.0.0-beta.2`) of plugins - for an update, in the version lists
|
|
7
|
+
* and in a plugin search. Stored in `localStorage`, so it is a preference of this browser, not a setting the server
|
|
8
|
+
* keeps: it only decides what the administrator is shown, since an administrator can already install any published
|
|
9
|
+
* version by name. Off unless it was turned on.
|
|
10
|
+
*
|
|
11
|
+
* A read that finds nothing, or a `localStorage` that throws (private-browsing/storage-blocked contexts), is off, and a
|
|
12
|
+
* write that fails is swallowed, so a blocked store only means the choice doesn't survive a reload.
|
|
13
|
+
*/
|
|
14
|
+
const STORAGE_KEY = "rapidmx:plugins-allow-prerelease";
|
|
15
|
+
export function getAllowPrerelease() {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(STORAGE_KEY) === "true";
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function setAllowPrerelease(allow) {
|
|
24
|
+
try {
|
|
25
|
+
if (allow) {
|
|
26
|
+
localStorage.setItem(STORAGE_KEY, "true");
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not persisted; the choice still applies until the page is reloaded.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
3
|
import { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
4
|
+
/** A conversation row of the list. `mailboxUid` is set only by a view that merges several mailboxes ("All Mailboxes"), whose rows are
|
|
5
|
+
* each from a different one; a list of one mailbox's own leaves it out and every row is that mailbox's. */
|
|
6
|
+
export type ListedConversation = ConversationSummary & {
|
|
7
|
+
mailboxUid?: string;
|
|
8
|
+
};
|
|
9
|
+
/** What tells a row apart from the others: the same conversation id in two mailboxes (a message sent to both) is two rows. */
|
|
10
|
+
export declare function conversationRowKey(conversation: ListedConversation): string;
|
|
4
11
|
export interface ConversationListProps {
|
|
5
|
-
conversations:
|
|
6
|
-
/** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. */
|
|
12
|
+
conversations: ListedConversation[];
|
|
13
|
+
/** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. A row that names its own mailbox uses that. */
|
|
7
14
|
mailboxUid: string;
|
|
8
15
|
/** The message currently open in the reading pane, so the row standing for it can be marked. */
|
|
9
16
|
selectedUid: string | null;
|
|
@@ -12,7 +19,7 @@ export interface ConversationListProps {
|
|
|
12
19
|
* stands for, or the conversation's own `latestMessageUid` for a parent row. The conversation goes
|
|
13
20
|
* with the uid because the pane shows the whole thread, not just that message.
|
|
14
21
|
*/
|
|
15
|
-
onOpenMessage: (conversation:
|
|
22
|
+
onOpenMessage: (conversation: ListedConversation, uid: string) => void;
|
|
16
23
|
/** Newer copies of messages this list already fetched - the reading pane marks the message it opens as
|
|
17
24
|
* read, which this list would otherwise keep showing as unread until the thread is collapsed and
|
|
18
25
|
* expanded again. */
|
|
@@ -13,6 +13,10 @@ import { listConversationMessages } from "@rapidmx/react-shared/mail/conversatio
|
|
|
13
13
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
|
|
14
14
|
import SwipeRow from "./SwipeRow.js";
|
|
15
15
|
import InviteRowChip from "./invite/InviteRowChip.js";
|
|
16
|
+
/** What tells a row apart from the others: the same conversation id in two mailboxes (a message sent to both) is two rows. */
|
|
17
|
+
export function conversationRowKey(conversation) {
|
|
18
|
+
return conversation.mailboxUid ? `${conversation.mailboxUid}\u0000${conversation.conversationId}` : conversation.conversationId;
|
|
19
|
+
}
|
|
16
20
|
/** Every participant as `Name <address>`, for the tooltip and the accessible text of the "+N" a crowded row folds them into. */
|
|
17
21
|
function participantList(conversation) {
|
|
18
22
|
return conversation.participants.map((participant) => formatMailAddress(participant)).join(", ");
|
|
@@ -41,7 +45,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
41
45
|
setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
|
|
42
46
|
}
|
|
43
47
|
function toggle(conversation) {
|
|
44
|
-
const id = conversation
|
|
48
|
+
const id = conversationRowKey(conversation);
|
|
45
49
|
setExpanded((prev) => {
|
|
46
50
|
const next = new Set(prev);
|
|
47
51
|
if (next.has(id)) {
|
|
@@ -61,7 +65,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
61
65
|
delete next[id];
|
|
62
66
|
return next;
|
|
63
67
|
});
|
|
64
|
-
listConversationMessages(mailboxUid,
|
|
68
|
+
listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId)
|
|
65
69
|
.then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
|
|
66
70
|
.catch((err) => setErrorsById((prev) => ({
|
|
67
71
|
...prev,
|
|
@@ -77,15 +81,15 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
|
|
|
77
81
|
return _jsx("p", { className: "p-4 text-sm text-text-muted", children: "No conversations in this folder." });
|
|
78
82
|
}
|
|
79
83
|
return (_jsx("ul", { className: swipe?.enabled ? "overflow-x-clip" : undefined, children: conversations.map((conversation) => {
|
|
80
|
-
const id = conversation
|
|
84
|
+
const id = conversationRowKey(conversation);
|
|
81
85
|
const isExpanded = expanded.has(id);
|
|
82
86
|
const unread = conversation.unreadCount > 0;
|
|
83
87
|
const loaded = messagesById[id];
|
|
84
88
|
// `listConversationMessages()` answers oldest first; the rows read in whichever sense the
|
|
85
89
|
// list itself is arranged in. Copied before reversing - the fetched array is cached.
|
|
86
90
|
const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
|
|
87
|
-
const panelId = `conversation-messages-${
|
|
88
|
-
const ticked = selectedConversationIds?.has(
|
|
91
|
+
const panelId = `conversation-messages-${conversation.mailboxUid ? `${conversation.mailboxUid}-` : ""}${conversation.conversationId}`;
|
|
92
|
+
const ticked = selectedConversationIds?.has(conversation.conversationId) ?? false;
|
|
89
93
|
return (_jsxs("li", { children: [_jsxs(SwipeRow, { as: "div", enabled: !!swipe?.enabled && !selectMode, onArchive: () => swipe.onArchive(conversation), onMove: () => swipe.onMove(conversation), "data-message-uid": conversation.latestMessageUid, "data-unread": unread ? "true" : undefined, className: rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch"), children: [_jsx(UnreadBar, { unread: unread }), selectMode && (_jsx("span", { className: "shrink-0 flex items-center pl-3", children: _jsx("input", { type: "checkbox", checked: ticked, onChange: () => onToggleSelected?.(conversation), "aria-label": `Select conversation: ${conversation.subject || "(no subject)"}`, className: "w-4 h-4 accent-primary" }) })), _jsx("button", { type: "button", onClick: () => toggle(conversation), "aria-expanded": isExpanded, "aria-controls": panelId, "aria-label": `${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`, className: ["shrink-0 px-2 text-text-muted hover:text-text", ROW_FOCUS_CLASS].join(" "), children: isExpanded ? (_jsx(HiChevronDown, { size: 16, "aria-hidden": "true" })) : (_jsx(HiChevronRight, { size: 16, "aria-hidden": "true" })) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col", children: [_jsxs("button", { type: "button", "data-row-open": true, onClick: () => onOpenMessage(conversation, conversation.latestMessageUid), className: ["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" "), children: [_jsx(UnreadLabel, { unread: unread }), _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: conversation.participants[0] ?? conversation.latestFrom, className: ["flex-1", senderClass(unread)].join(" ") }), conversation.participants.length > 1 && (_jsxs("span", { className: "text-xs text-text-muted shrink-0 font-normal", title: participantList(conversation), children: ["+", conversation.participants.length - 1, _jsxs("span", { className: "sr-only", children: [" more: ", participantList(conversation)] })] })), _jsx("span", { className: ["text-xs shrink-0", dateClass(unread)].join(" "), children: new Date(conversation.latestDate).toLocaleDateString() })] }), _jsx("div", { className: ["text-sm truncate", subjectClass(unread)].join(" "), children: conversation.subject || "(no subject)" }), _jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: conversationLooksEncrypted(conversation) ? _jsx(EncryptedPreview, {}) : conversation.latestPreview }), _jsxs("div", { className: "flex items-center gap-2 mt-1 text-xs text-text-muted font-normal", children: [conversation.messageCount > 1 && _jsxs("span", { children: [conversation.messageCount, " messages"] }), unread && conversation.messageCount > 1 && (_jsxs("span", { className: "py-0.5 px-2 rounded-pill bg-primary/15 text-primary-dark font-bold", children: [conversation.unreadCount, " unread"] })), conversation.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 13, "aria-label": "Has attachments" })), conversation.flagged && (_jsx(HiOutlineFlag, { size: 13, "aria-label": "Flagged", className: "text-danger" }))] })] }), _jsx(InviteRowChip, { message: {
|
|
90
94
|
uid: conversation.latestMessageUid,
|
|
91
95
|
meetingMethod: conversation.latestMeetingMethod ?? undefined,
|
|
@@ -89,7 +89,7 @@ export declare const CONVERSATION_SORT_NOTE = "Conversations are ordered within
|
|
|
89
89
|
* order that would mean nothing. Ties break newest first whichever direction is in force, so two rows the
|
|
90
90
|
* key can't tell apart still read in a stable, familiar order.
|
|
91
91
|
*/
|
|
92
|
-
export declare function sortConversations(conversations:
|
|
92
|
+
export declare function sortConversations<T extends ConversationSummary>(conversations: T[], sortBy: MessageListSort, sortOrder: MessageSortOrder): T[];
|
|
93
93
|
/** The direction the server itself would pick for a key if none were sent (see `MessageSortOrder`) - what
|
|
94
94
|
* this list switches to when the reader picks a different field, so changing "Date" to "Subject" reads A-Z
|
|
95
95
|
* rather than inheriting Date's newest-first as a surprising Z-A. */
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { attachmentSource, makeCidResolver } from "./bodyContent.js";
|
|
2
2
|
import { prepareBodyHtml } from "./bodyHtml.js";
|
|
3
3
|
import { frameCsp } from "./frameDocument.js";
|
|
4
|
-
const PRINT_CSS = `
|
|
5
|
-
html{font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#000;background:#fff}
|
|
6
|
-
body{margin:0;padding:24px}
|
|
7
|
-
h1{font-size:20px;margin:0 0 12px}
|
|
8
|
-
dl{margin:0 0 12px;display:grid;grid-template-columns:max-content 1fr;gap:2px 12px}
|
|
9
|
-
dt{font-weight:600;color:#444}
|
|
10
|
-
dd{margin:0;overflow-wrap:anywhere}
|
|
11
|
-
hr{border:0;border-top:1px solid #999;margin:0 0 16px}
|
|
12
|
-
#rr-body{overflow-wrap:break-word}
|
|
13
|
-
#rr-body img{max-width:100%;height:auto}
|
|
14
|
-
#rr-body pre,pre.rr-text{white-space:pre-wrap;font:inherit;margin:0}
|
|
4
|
+
const PRINT_CSS = `
|
|
5
|
+
html{font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#000;background:#fff}
|
|
6
|
+
body{margin:0;padding:24px}
|
|
7
|
+
h1{font-size:20px;margin:0 0 12px}
|
|
8
|
+
dl{margin:0 0 12px;display:grid;grid-template-columns:max-content 1fr;gap:2px 12px}
|
|
9
|
+
dt{font-weight:600;color:#444}
|
|
10
|
+
dd{margin:0;overflow-wrap:anywhere}
|
|
11
|
+
hr{border:0;border-top:1px solid #999;margin:0 0 16px}
|
|
12
|
+
#rr-body{overflow-wrap:break-word}
|
|
13
|
+
#rr-body img{max-width:100%;height:auto}
|
|
14
|
+
#rr-body pre,pre.rr-text{white-space:pre-wrap;font:inherit;margin:0}
|
|
15
15
|
`;
|
|
16
16
|
export function escapeHtml(text) {
|
|
17
17
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|