nexus-shared 4.0.0 → 4.1.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/CHANGELOG.md +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { iconPencil, iconSend, iconTrash, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { emptyMailDraft, mailDraftHasText, readMailDraft, writeMailDraft } from "../../Helpers/MailClientHelpers.js";
|
|
6
|
+
import { MAIL_BODY_MAX, MAIL_SUBJECT_MAX, mailPeoplePicker, newMailClientId } from "../../Helpers/MessageHelpers.js";
|
|
7
|
+
import { holdPickerItems } from "../../Helpers/PagedPicker.js";
|
|
8
|
+
import { Button } from "../Documents/Button.js";
|
|
9
|
+
import { CHAT_TOOLBAR } from "../Chats/ChatComposer.js";
|
|
10
|
+
import { ChatFailure } from "../Chats/ChatParts.js";
|
|
11
|
+
import { Dropdown } from "../Inputs/Dropdown.js";
|
|
12
|
+
import { MarkdownEditor } from "../Inputs/MarkdownEditor.js";
|
|
13
|
+
import { TextBox } from "../Inputs/TextBox.js";
|
|
14
|
+
import { showMessageBox } from "../Layouts/MessageBox.js";
|
|
15
|
+
import { Popup } from "../Layouts/Popup.js";
|
|
16
|
+
import { MailPeople } from "./MailThread.js";
|
|
17
|
+
/** The smallest the body's editor gets, however short the window. */
|
|
18
|
+
const MIN_BODY = 120;
|
|
19
|
+
/** A new mail or a reply, in a docked window. */
|
|
20
|
+
export function MailComposeWindow({ open, onClose, source, userId, reply, initial, useDefaults = true, picker, draftKey, onSent, windowState, onWindowStateChange, className, }) {
|
|
21
|
+
const key = draftKey ?? (reply ? `reply:${String(reply.threadId)}` : "new");
|
|
22
|
+
// What the window holds, kept in a ref too so a close asks about what is there now.
|
|
23
|
+
const held = useRef(emptyMailDraft());
|
|
24
|
+
async function askToClose() {
|
|
25
|
+
if (!mailDraftHasText(held.current)) {
|
|
26
|
+
writeMailDraft(userId, key, null);
|
|
27
|
+
onClose();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const answer = await showMessageBox({
|
|
31
|
+
title: "Keep this message?",
|
|
32
|
+
message: "It is not sent. Keep it as a draft to finish later, or discard it.",
|
|
33
|
+
tone: "question",
|
|
34
|
+
buttons: [
|
|
35
|
+
{ value: "discard", label: "Discard", variant: "secondary" },
|
|
36
|
+
{ value: "keep", label: "Keep draft", variant: "primary", autoFocus: true },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
if (answer === null)
|
|
40
|
+
return;
|
|
41
|
+
if (answer === "discard")
|
|
42
|
+
writeMailDraft(userId, key, null);
|
|
43
|
+
onClose();
|
|
44
|
+
}
|
|
45
|
+
return (_jsx(Popup, { open: open, onClose: () => void askToClose(), title: reply ? `Reply: ${reply.subject}` : "New message", icon: _jsx(InlineIcon, { icon: reply ? iconSend : iconPencil }), size: "md", height: "36rem", docked: true, minimizable: true, movable: false, reopen: "restore", flush: true, windowState: windowState, onWindowStateChange: onWindowStateChange, className: className, children: _jsx(MailComposeForm, { source: source, userId: userId, reply: reply ?? null, initial: initial, useDefaults: useDefaults, picker: picker, draftKey: key, held: held, onDiscard: () => {
|
|
46
|
+
writeMailDraft(userId, key, null);
|
|
47
|
+
onClose();
|
|
48
|
+
}, onSent: result => {
|
|
49
|
+
writeMailDraft(userId, key, null);
|
|
50
|
+
onSent?.(result);
|
|
51
|
+
onClose();
|
|
52
|
+
} }) }));
|
|
53
|
+
}
|
|
54
|
+
// The window's content: mounted each time it opens, so it reads the kept draft then.
|
|
55
|
+
function MailComposeForm({ source, userId, reply, initial, useDefaults, picker: givenPicker, draftKey, held, onDiscard, onSent }) {
|
|
56
|
+
const picker = useMemo(() => givenPicker ?? mailPeoplePicker(source), [givenPicker, source]);
|
|
57
|
+
const [draft, setDraft] = useState(() => {
|
|
58
|
+
const kept = readMailDraft(userId, draftKey);
|
|
59
|
+
if (kept)
|
|
60
|
+
return kept;
|
|
61
|
+
return { ...emptyMailDraft(), ...(!reply && initial && { to: [...(initial.to ?? [])], cc: [...(initial.cc ?? [])], bcc: [...(initial.bcc ?? [])], subject: initial.subject ?? "" }), body: initial?.body ?? "" };
|
|
62
|
+
});
|
|
63
|
+
const [showCopies, setShowCopies] = useState(() => draft.cc.length > 0 || draft.bcc.length > 0);
|
|
64
|
+
const [clientId] = useState(newMailClientId);
|
|
65
|
+
const [sending, setSending] = useState(false);
|
|
66
|
+
const [error, setError] = useState(null);
|
|
67
|
+
const [tried, setTried] = useState(false);
|
|
68
|
+
held.current = draft;
|
|
69
|
+
// To from the reader's defaults, for a new mail with nobody named yet.
|
|
70
|
+
const asked = useRef(false);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (reply || !useDefaults || asked.current || draft.to.length > 0 || draft.cc.length > 0 || draft.bcc.length > 0)
|
|
73
|
+
return;
|
|
74
|
+
asked.current = true;
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
source.defaults(controller.signal).then(defaults => {
|
|
77
|
+
holdPickerItems(picker, [...defaults.to, ...defaults.cc, ...defaults.bcc]);
|
|
78
|
+
const ids = (people) => people.filter(person => person.isActive !== false).map(person => person.id);
|
|
79
|
+
setDraft(was => (was.to.length || was.cc.length || was.bcc.length ? was : { ...was, to: ids(defaults.to), cc: ids(defaults.cc), bcc: ids(defaults.bcc) }));
|
|
80
|
+
if (defaults.cc.length || defaults.bcc.length)
|
|
81
|
+
setShowCopies(true);
|
|
82
|
+
}, () => undefined);
|
|
83
|
+
return () => controller.abort();
|
|
84
|
+
// Asked once per opening.
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, []);
|
|
87
|
+
// Kept as it is typed, a moment after the last key.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const timer = setTimeout(() => writeMailDraft(userId, draftKey, { ...draft, savedAt: Date.now() }), 300);
|
|
90
|
+
return () => clearTimeout(timer);
|
|
91
|
+
}, [draft, userId, draftKey]);
|
|
92
|
+
// The body's editor fills what the window leaves it, as the window is resized or maximised.
|
|
93
|
+
const room = useRef(null);
|
|
94
|
+
const [bodyHeight, setBodyHeight] = useState(200);
|
|
95
|
+
const heightRef = useRef(bodyHeight);
|
|
96
|
+
heightRef.current = bodyHeight;
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
const element = room.current;
|
|
99
|
+
if (!element)
|
|
100
|
+
return;
|
|
101
|
+
const measure = () => {
|
|
102
|
+
const editor = element.firstElementChild;
|
|
103
|
+
if (!editor || element.clientHeight <= 0)
|
|
104
|
+
return;
|
|
105
|
+
// The room inside the padding, less what the editor draws around its text: its bar and its status line.
|
|
106
|
+
const style = getComputedStyle(element);
|
|
107
|
+
const room = element.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
|
|
108
|
+
const chrome = editor.offsetHeight - heightRef.current;
|
|
109
|
+
const next = Math.max(MIN_BODY, Math.floor(room - chrome));
|
|
110
|
+
if (Math.abs(next - heightRef.current) > 1)
|
|
111
|
+
setBodyHeight(next);
|
|
112
|
+
};
|
|
113
|
+
measure();
|
|
114
|
+
const observer = new ResizeObserver(measure);
|
|
115
|
+
observer.observe(element);
|
|
116
|
+
return () => observer.disconnect();
|
|
117
|
+
}, []);
|
|
118
|
+
const set = (changes) => setDraft(was => ({ ...was, ...changes }));
|
|
119
|
+
const problems = [];
|
|
120
|
+
if (!reply && draft.to.length === 0)
|
|
121
|
+
problems.push("Name at least one person in To.");
|
|
122
|
+
if (!reply && draft.subject.trim() === "")
|
|
123
|
+
problems.push("Write a subject.");
|
|
124
|
+
if (draft.body.trim() === "")
|
|
125
|
+
problems.push("Write the message.");
|
|
126
|
+
async function send() {
|
|
127
|
+
setTried(true);
|
|
128
|
+
setError(null);
|
|
129
|
+
if (problems.length > 0)
|
|
130
|
+
return;
|
|
131
|
+
setSending(true);
|
|
132
|
+
try {
|
|
133
|
+
if (reply) {
|
|
134
|
+
await source.threadSource(reply.threadId).send({ body: draft.body.trim(), clientId, kind: "mail" });
|
|
135
|
+
onSent({ threadId: reply.threadId, reply: true });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const sent = await source.compose({ to: draft.to, cc: draft.cc, bcc: draft.bcc, subject: draft.subject.trim(), body: draft.body.trim(), clientId });
|
|
139
|
+
onSent({ threadId: sent.id, reply: false });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (failure) {
|
|
143
|
+
setError(failure instanceof Error && failure.message ? failure.message : "The message was not sent.");
|
|
144
|
+
setSending(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const people = (name, label, autoFocus) => (_jsx(Dropdown, { multiple: true, name: `nx-mail-compose-${name}`, label: label, size: "sm", placeholder: "Search by name", value: draft[name], ...picker, onValueChange: (values) => set({ [name]: values }), error: tried && name === "to" && draft.to.length === 0 ? "Name at least one person." : undefined, autoFocus: autoFocus }));
|
|
148
|
+
return (_jsxs("div", { className: "nx-mail-compose", children: [_jsx("div", { className: "nx-mail-compose__fields", children: reply ? (_jsxs("p", { className: "nx-mail-compose__to", children: [_jsx("span", { className: "nx-mail-compose__label", children: "To" }), _jsx(MailPeople, { people: reply.people, userId: userId })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "nx-mail-compose__line", children: [people("to", "To", draft.to.length === 0), !showCopies && (_jsx(Button, { size: "sm", variant: "ghost", className: "nx-mail-compose__copies", onClick: () => setShowCopies(true), children: "Cc / Bcc" }))] }), showCopies && (_jsxs("div", { className: "nx-mail-compose__line", children: [people("cc", "Cc"), people("bcc", "Bcc")] })), _jsx(TextBox, { name: "nx-mail-compose-subject", label: "Subject", size: "sm", value: draft.subject, maxLength: MAIL_SUBJECT_MAX, onValueChange: subject => set({ subject }), error: tried && draft.subject.trim() === "" ? "Write a subject." : undefined })] })) }), _jsx("div", { ref: room, className: "nx-mail-compose__body", children: _jsx(MarkdownEditor, { name: "nx-mail-compose-body", "aria-label": "Message", placeholder: "Write your message\u2026", defaultValue: draft.body, modes: ["write", "preview"], toolbar: CHAT_TOOLBAR, toolbarOverflow: "menu", height: bodyHeight, minHeight: MIN_BODY, resizable: false, fullscreen: false, maxLength: MAIL_BODY_MAX, autoFocus: !!reply || draft.to.length > 0, error: tried && draft.body.trim() === "" ? "Write the message." : undefined, onValueChange: body => set({ body }), onKeyDown: event => {
|
|
149
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
void send();
|
|
152
|
+
}
|
|
153
|
+
} }) }), error && _jsx(ChatFailure, { message: error }), _jsxs("footer", { className: "nx-mail-compose__foot", children: [_jsx(Button, { variant: "ghost", hoverTone: "danger", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": "Discard draft", title: "Discard draft", onClick: onDiscard, disabled: sending }), _jsx("span", { className: "nx-mail-compose__saved", "aria-live": "polite", children: mailDraftHasText(draft) ? "Draft kept on this device" : "" }), _jsx(Button, { variant: "primary", icon: _jsx(InlineIcon, { icon: iconSend }), loading: sending, onClick: () => void send(), title: "Send (Ctrl+Enter)", children: "Send" })] })] }));
|
|
154
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ChatId, ChatSummary } from "../../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
import type { MailDomain, MailInboxRow, MailThreadHead, MessageSource } from "../../Interfaces/MailInterfaces.ts";
|
|
4
|
+
import type { MailDomainContext } from "./MailThread.tsx";
|
|
5
|
+
/** What Discuss and Reply act on. */
|
|
6
|
+
export interface MailReaderAction {
|
|
7
|
+
head: MailThreadHead;
|
|
8
|
+
/** The thread's chat lines held so far. */
|
|
9
|
+
discussion: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MailReaderProps {
|
|
12
|
+
/** Where the mailbox's data comes from. */
|
|
13
|
+
source: MessageSource;
|
|
14
|
+
/** The conversation. */
|
|
15
|
+
threadId: ChatId;
|
|
16
|
+
/** Who is reading. */
|
|
17
|
+
userId: ChatId;
|
|
18
|
+
/** The list's row, drawn at once while the head is read. */
|
|
19
|
+
row?: MailInboxRow | null;
|
|
20
|
+
/** Discuss (n) on a mail, Open chat on a chat: the page opens the chat popup. Without it, no button. */
|
|
21
|
+
onDiscuss?: (action: MailReaderAction) => void;
|
|
22
|
+
/** Reply: the page opens a compose window for a mail in the thread. Without it, or when the reader may not write, no button. */
|
|
23
|
+
onReply?: (action: MailReaderAction) => void;
|
|
24
|
+
/** Buttons at the end of the subject's line: Archive, Move to inbox. */
|
|
25
|
+
actions?: ReactNode;
|
|
26
|
+
/** Draws the record a thread is about. Default its `DomainCard`, folded under 480px. */
|
|
27
|
+
renderDomain?: (domain: MailDomain, context: MailDomainContext) => ReactNode;
|
|
28
|
+
/** Grows when the page knows the thread changed (a reply sent from a compose window): the lines are read again. */
|
|
29
|
+
revision?: number;
|
|
30
|
+
/** The subject's heading level. Default 2. */
|
|
31
|
+
headingLevel?: 2 | 3 | 4;
|
|
32
|
+
/** Every time the head is read. */
|
|
33
|
+
onHead?: (head: MailThreadHead) => void;
|
|
34
|
+
/** Every time the counts change, a read thread included: the list's row clears its unread count. */
|
|
35
|
+
onSummary?: (summary: ChatSummary) => void;
|
|
36
|
+
/** Lines per page. Default 30. */
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
/** Milliseconds between checks for new lines. 0 turns it off. Default 15,000. */
|
|
39
|
+
pollInterval?: number;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A conversation read as mail. */
|
|
43
|
+
export declare function MailReader({ source, threadId, userId, row, onDiscuss, onReply, actions, renderDomain, revision, headingLevel, onHead, onSummary, pageSize, pollInterval, className, }: MailReaderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { iconArrowBackUp, iconArrowRight, iconChevronDown, iconChevronRight, iconMessageCircle, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { flushSync } from "react-dom";
|
|
6
|
+
import { chatDayLabel, chatLocalDate, chatPreviewText, chatTimeLabel, sameChatId } from "../../Helpers/ChatHelpers.js";
|
|
7
|
+
import { useChat } from "../../Helpers/ChatHooks.js";
|
|
8
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
9
|
+
import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
10
|
+
import { formatDateTime } from "../../Helpers/DateTimeHelpers.js";
|
|
11
|
+
import { mailDiscussionCount, mailRowKind, mailTypeLabel } from "../../Helpers/MailClientHelpers.js";
|
|
12
|
+
import { MAIL_ROLE_LABELS } from "../../Helpers/MessageHelpers.js";
|
|
13
|
+
import { Button } from "../Documents/Button.js";
|
|
14
|
+
import { ChatEventNote, ChatFailure } from "../Chats/ChatParts.js";
|
|
15
|
+
import { DomainCard } from "../Domains/DomainCard.js";
|
|
16
|
+
import { MarkdownView } from "../Viewers/MarkdownView.js";
|
|
17
|
+
import { Avatar } from "../Viewers/Avatar.js";
|
|
18
|
+
import { MailPeople } from "./MailThread.js";
|
|
19
|
+
const REFUSED = "This message is not available to you.";
|
|
20
|
+
const NARROW = 480;
|
|
21
|
+
const errorStatus = (error) => {
|
|
22
|
+
const status = error?.status;
|
|
23
|
+
return typeof status === "number" ? status : undefined;
|
|
24
|
+
};
|
|
25
|
+
/** A head drawn from the list's row while the thread's own is read: the subject and the sender, nobody else yet. */
|
|
26
|
+
function headFromRow(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: row.id,
|
|
29
|
+
subject: row.subject,
|
|
30
|
+
from: { id: row.fromId, name: row.fromName, ...(row.fromAvatar && { avatar: row.fromAvatar }) },
|
|
31
|
+
to: [],
|
|
32
|
+
cc: [],
|
|
33
|
+
bcc: [],
|
|
34
|
+
myRole: row.myRole,
|
|
35
|
+
archived: row.archived,
|
|
36
|
+
canSend: true,
|
|
37
|
+
createdTime: row.lastMessageTime,
|
|
38
|
+
messageCount: row.messageCount,
|
|
39
|
+
unreadCount: row.unreadCount,
|
|
40
|
+
...(row.threadKind && { threadKind: row.threadKind }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** A conversation read as mail. */
|
|
44
|
+
export function MailReader({ source, threadId, userId, row, onDiscuss, onReply, actions, renderDomain, revision = 0, headingLevel = 2, onHead, onSummary, pageSize, pollInterval, className, }) {
|
|
45
|
+
const [head, setHead] = useState(null);
|
|
46
|
+
const [headState, setHeadState] = useState({ status: "loading" });
|
|
47
|
+
const [attempt, setAttempt] = useState(0);
|
|
48
|
+
const [cardRevision, setCardRevision] = useState(0);
|
|
49
|
+
const latest = useRef({ onHead, onSummary });
|
|
50
|
+
latest.current = { onHead, onSummary };
|
|
51
|
+
const chatSource = useMemo(() => source.threadSource(threadId), [source, threadId]);
|
|
52
|
+
const scope = useMemo(() => ({ domain: "mail-reader", parentId: threadId, userId }), [threadId, userId]);
|
|
53
|
+
const refused = headState.status === "refused";
|
|
54
|
+
const { store, state } = useChat({ scope, conversationId: threadId, source: chatSource, pageSize, pollInterval, enabled: !refused });
|
|
55
|
+
// The head, read whenever the thread changes, and again on Retry; the list's row stands in while it comes.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
setHead(null);
|
|
59
|
+
setHeadState({ status: "loading" });
|
|
60
|
+
source.thread(threadId, controller.signal).then(next => {
|
|
61
|
+
setHead(next);
|
|
62
|
+
setHeadState({ status: "ready" });
|
|
63
|
+
latest.current.onHead?.(next);
|
|
64
|
+
}, error => {
|
|
65
|
+
if (controller.signal.aborted)
|
|
66
|
+
return;
|
|
67
|
+
const status = errorStatus(error);
|
|
68
|
+
if (status === 403 || status === 404)
|
|
69
|
+
setHeadState({ status: "refused" });
|
|
70
|
+
else
|
|
71
|
+
setHeadState({ status: "failed", message: error instanceof Error && error.message ? error.message : "The message could not be loaded." });
|
|
72
|
+
});
|
|
73
|
+
return () => controller.abort();
|
|
74
|
+
}, [source, threadId, attempt]);
|
|
75
|
+
// A reply sent from a compose window: the lines and the head are read again, the record's card too.
|
|
76
|
+
const seen = useRef(revision);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (seen.current === revision)
|
|
79
|
+
return;
|
|
80
|
+
seen.current = revision;
|
|
81
|
+
void store.refresh();
|
|
82
|
+
setCardRevision(count => count + 1);
|
|
83
|
+
source.thread(threadId).then(next => {
|
|
84
|
+
setHead(next);
|
|
85
|
+
latest.current.onHead?.(next);
|
|
86
|
+
}, () => undefined);
|
|
87
|
+
}, [revision, store, source, threadId]);
|
|
88
|
+
// Opening a conversation reads it: once its lines are in, the read mark goes to the newest.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (state.loaded && state.summary.unreadCount > 0)
|
|
91
|
+
void store.markRead();
|
|
92
|
+
}, [state.loaded, state.summary.unreadCount, store]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (state.loaded)
|
|
95
|
+
latest.current.onSummary?.(state.summary);
|
|
96
|
+
}, [state.loaded, state.summary]);
|
|
97
|
+
// A notification arriving reads the record's card again: the module wrote it after changing the record.
|
|
98
|
+
const notes = state.messages.filter(message => message.kind === "notification").length;
|
|
99
|
+
const notesSeen = useRef(null);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!state.loaded)
|
|
102
|
+
return;
|
|
103
|
+
if (notesSeen.current !== null && notes > notesSeen.current)
|
|
104
|
+
setCardRevision(count => count + 1);
|
|
105
|
+
notesSeen.current = notes;
|
|
106
|
+
}, [notes, state.loaded]);
|
|
107
|
+
// How wide the pane is: a record's card opens folded in a narrow one.
|
|
108
|
+
const frame = useRef(null);
|
|
109
|
+
const [narrow, setNarrow] = useState(null);
|
|
110
|
+
const [folded, setFolded] = useState(null);
|
|
111
|
+
useLayoutEffect(() => {
|
|
112
|
+
const element = frame.current;
|
|
113
|
+
if (!element)
|
|
114
|
+
return;
|
|
115
|
+
let known = false;
|
|
116
|
+
const read = () => {
|
|
117
|
+
const width = element.clientWidth;
|
|
118
|
+
if (width <= 0)
|
|
119
|
+
return;
|
|
120
|
+
if (known)
|
|
121
|
+
setNarrow(width < NARROW);
|
|
122
|
+
else
|
|
123
|
+
flushSync(() => setNarrow(width < NARROW));
|
|
124
|
+
known = true;
|
|
125
|
+
};
|
|
126
|
+
// The first width, read here, is set as any state; a later one from the observer is drawn before the browser paints.
|
|
127
|
+
if (element.clientWidth > 0) {
|
|
128
|
+
setNarrow(element.clientWidth < NARROW);
|
|
129
|
+
known = true;
|
|
130
|
+
}
|
|
131
|
+
const observer = new ResizeObserver(read);
|
|
132
|
+
observer.observe(element);
|
|
133
|
+
return () => observer.disconnect();
|
|
134
|
+
}, [refused]);
|
|
135
|
+
const preferences = useDatePreferences();
|
|
136
|
+
const zone = useBrowserTimeZone();
|
|
137
|
+
const time = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }) }), [preferences, zone]);
|
|
138
|
+
const shown = head ?? (row && sameChatId(row.id, threadId) ? headFromRow(row) : null);
|
|
139
|
+
const kind = shown?.threadKind ?? (row ? mailRowKind(row) : shown?.domain ? "domain" : "mail");
|
|
140
|
+
const discussion = mailDiscussionCount(state.messages);
|
|
141
|
+
const action = useCallback(() => (shown ? { head: shown, discussion } : null), [shown, discussion]);
|
|
142
|
+
const Heading = `h${headingLevel}`;
|
|
143
|
+
if (refused) {
|
|
144
|
+
return (_jsx("section", { className: cx("nx-mail-reader", className), children: _jsx("div", { className: "nx-mail__refused", role: "alert", children: _jsx("p", { children: REFUSED }) }) }));
|
|
145
|
+
}
|
|
146
|
+
const typeLabel = mailTypeLabel(kind, shown?.domain?.domainType ?? row?.domainType ?? null);
|
|
147
|
+
const mails = state.messages.filter(message => message.kind === "mail" || message.kind === "notification" || (kind !== "chat" && message === state.messages[0] && !message.kind));
|
|
148
|
+
const chats = state.messages.filter(message => (message.kind ?? "chat") === "chat");
|
|
149
|
+
const lastChat = chats[chats.length - 1];
|
|
150
|
+
const waiting = !state.loaded && state.loading;
|
|
151
|
+
const canReply = !!head && head.canSend && kind !== "chat";
|
|
152
|
+
const domain = shown?.domain &&
|
|
153
|
+
(renderDomain ? (renderDomain(shown.domain, { head: shown, revision: cardRevision })) : narrow === null ? null : (_jsx(DomainCard, { domain: shown.domain.domainType, id: shown.domain.id, label: shown.domain.label, revision: cardRevision, viewer: userId, headingLevel: (headingLevel + 1), collapsed: folded ?? narrow, onCollapsedChange: setFolded })));
|
|
154
|
+
return (_jsxs("section", { ref: frame, className: cx("nx-mail-reader", className), "data-kind": kind, "aria-busy": headState.status === "loading" || waiting || undefined, children: [_jsxs("header", { className: "nx-mail-reader__head", children: [_jsxs("div", { className: "nx-mail__title", children: [_jsx(Heading, { className: "nx-mail__subject", tabIndex: -1, "data-nx-focus": "", children: shown ? shown.subject || "(no subject)" : _jsx("span", { className: "nx-table__bone nx-record__bone nx-mail__bone", style: { width: "16rem" } }) }), _jsx("span", { className: "nx-table__tag nx-table__tag--plain nx-mailbox__chip", "data-kind": kind, children: typeLabel }), actions && _jsx("span", { className: "nx-mail__actions", children: actions })] }), shown && _jsx(MailHeadLine, { head: shown, kind: kind, userId: userId, time: time, known: !!head }), headState.status === "failed" && _jsx(ChatFailure, { message: headState.message, onRetry: () => setAttempt(count => count + 1) })] }), domain && _jsx("div", { className: "nx-mail-reader__domain", children: domain }), _jsxs("div", { className: "nx-mail-reader__body", children: [state.error && !state.loaded && _jsx(ChatFailure, { message: state.error, onRetry: () => void store.load() }), waiting && _jsx(MailBones, {}), kind === "chat" ? (state.loaded && _jsx(MailChatHead, { last: lastChat, count: chats.length, time: time })) : (_jsxs(_Fragment, { children: [state.hasMore && state.loaded && (_jsx("button", { type: "button", className: "nx-mail-reader__older", disabled: state.loadingOlder, onClick: () => void store.loadOlder(), children: state.loadingOlder ? "Loading earlier messages…" : "Show earlier messages" })), state.loaded && mails.length === 0 && _jsx("p", { className: "nx-mail-reader__empty", children: "This conversation has no mail yet: its messages are in the discussion." }), _jsx(MailItems, { mails: mails, userId: userId, time: time })] }))] }), (onDiscuss || (onReply && canReply)) && shown && (_jsxs("footer", { className: "nx-mail-reader__foot", children: [onDiscuss && (_jsx(Button, { variant: kind === "chat" ? "primary" : "secondary", icon: _jsx(InlineIcon, { icon: iconMessageCircle }), onClick: () => {
|
|
155
|
+
const next = action();
|
|
156
|
+
if (next)
|
|
157
|
+
onDiscuss(next);
|
|
158
|
+
}, children: kind === "chat" ? "Open chat" : `Discuss${state.loaded ? ` (${discussion}${state.hasMore ? "+" : ""})` : ""}` })), onReply && canReply && (_jsx(Button, { icon: _jsx(InlineIcon, { icon: iconArrowBackUp }), onClick: () => {
|
|
159
|
+
const next = action();
|
|
160
|
+
if (next)
|
|
161
|
+
onReply(next);
|
|
162
|
+
}, children: "Reply" }))] }))] }));
|
|
163
|
+
}
|
|
164
|
+
/** When, on one line: the day and the time in the reader's settings, the full moment on hover. */
|
|
165
|
+
function MailWhen({ value, time }) {
|
|
166
|
+
if (!value)
|
|
167
|
+
return null;
|
|
168
|
+
const day = chatLocalDate(value, time.timeZone);
|
|
169
|
+
return (_jsx("time", { className: "nx-mail__time", dateTime: value, title: formatDateTime(value, time), children: day ? `${chatDayLabel(day, time)}, ${chatTimeLabel(value, time)}` : chatTimeLabel(value, time) }));
|
|
170
|
+
}
|
|
171
|
+
/** From → To · Cc · Bcc, and when it started: a letter's head, on as few lines as the names need. */
|
|
172
|
+
function MailHeadLine({ head, kind, userId, time, known }) {
|
|
173
|
+
const others = kind === "chat" ? [...head.to, ...head.cc] : [];
|
|
174
|
+
return (_jsxs("div", { className: "nx-mail-reader__people", children: [_jsx(Avatar, { name: head.from.name, src: head.from.avatar }), _jsxs("p", { className: "nx-mail-reader__line", children: [_jsx("span", { className: "nx-mail-reader__from", children: head.from.name }), kind === "chat" ? (known && others.length > 0 && (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconArrowRight, className: "nx-mail-reader__arrow" }), _jsx(MailPeople, { people: others, userId: userId })] }))) : (_jsxs(_Fragment, { children: [known && (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconArrowRight, className: "nx-mail-reader__arrow" }), _jsx(MailPeople, { people: head.to, userId: userId })] })), head.cc.length > 0 && (_jsxs("span", { className: "nx-mail-reader__role", children: [_jsx("span", { className: "nx-mail-reader__dot", "aria-hidden": "true", children: "\u00B7" }), MAIL_ROLE_LABELS.cc, " ", _jsx(MailPeople, { people: head.cc, userId: userId })] })), head.bcc.length > 0 && (_jsxs("span", { className: "nx-mail-reader__role", children: [_jsx("span", { className: "nx-mail-reader__dot", "aria-hidden": "true", children: "\u00B7" }), MAIL_ROLE_LABELS.bcc, " ", _jsx(MailPeople, { people: head.bcc, userId: userId })] }))] })), _jsxs("span", { className: "nx-mail-reader__since", children: [kind === "chat" ? "Started " : "", _jsx(MailWhen, { value: head.createdTime, time: time })] })] })] }));
|
|
175
|
+
}
|
|
176
|
+
/** The mails of a thread: the newest open, the earlier ones folded to a line each that opens in place. */
|
|
177
|
+
function MailItems({ mails, userId, time }) {
|
|
178
|
+
const [opened, setOpened] = useState(() => new Set());
|
|
179
|
+
const newest = mails[mails.length - 1];
|
|
180
|
+
return (_jsx("ol", { className: "nx-mail-reader__mails", children: mails.map(message => {
|
|
181
|
+
const key = String(message.clientId ?? message.id);
|
|
182
|
+
if (message.kind === "notification") {
|
|
183
|
+
return (_jsx("li", { className: "nx-mail-reader__note", children: _jsx(ChatEventNote, { message: message, time: time }) }, key));
|
|
184
|
+
}
|
|
185
|
+
const open = message === newest || opened.has(key);
|
|
186
|
+
const toggle = () => setOpened(was => {
|
|
187
|
+
const next = new Set(was);
|
|
188
|
+
if (next.has(key))
|
|
189
|
+
next.delete(key);
|
|
190
|
+
else
|
|
191
|
+
next.add(key);
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
const name = message.authorName || "Unknown";
|
|
195
|
+
return (_jsxs("li", { className: "nx-mail-reader__mail", children: [message === newest ? (_jsxs("div", { className: "nx-mail-reader__mail-head", children: [_jsxs("span", { className: "nx-mail-reader__author", children: [name, sameChatId(message.authorId, userId) && _jsx("span", { className: "nx-mail__you", children: "you" })] }), _jsx(MailWhen, { value: message.createdAt, time: time })] })) : (_jsxs("button", { type: "button", className: "nx-mail-reader__mail-head", "aria-expanded": open, onClick: toggle, children: [_jsx(InlineIcon, { icon: open ? iconChevronDown : iconChevronRight, className: "nx-mail-reader__fold" }), _jsx("span", { className: "nx-mail-reader__author", children: name }), !open && _jsx("span", { className: "nx-mail-reader__preview", children: chatPreviewText(message.body, 140) }), _jsx(MailWhen, { value: message.createdAt, time: time })] })), open && _jsx(MarkdownView, { className: "nx-mail-reader__text", value: message.body })] }, key));
|
|
196
|
+
}) }));
|
|
197
|
+
}
|
|
198
|
+
/** A chat read in the pane: how many lines it holds, and its last one, while the chat itself is in its popup. */
|
|
199
|
+
function MailChatHead({ last, count, time }) {
|
|
200
|
+
if (!last)
|
|
201
|
+
return _jsx("p", { className: "nx-mail-reader__empty", children: "No messages yet. Open the chat to write the first." });
|
|
202
|
+
return (_jsxs("div", { children: [_jsxs("p", { className: "nx-mail-reader__count", children: [count === 1 ? "1 message" : `${count} messages`, " \u00B7 the last:"] }), _jsxs("div", { className: "nx-mail-reader__mail-head", children: [_jsx("span", { className: "nx-mail-reader__author", children: last.authorName || "Unknown" }), _jsx(MailWhen, { value: last.createdAt, time: time })] }), _jsx(MarkdownView, { className: "nx-mail-reader__text", value: last.body })] }));
|
|
203
|
+
}
|
|
204
|
+
/** The bones of a mail while its lines load: an author's line and three of text. */
|
|
205
|
+
function MailBones() {
|
|
206
|
+
return (_jsxs("div", { className: "nx-mail-reader__bones", "aria-hidden": "true", children: [_jsx("span", { className: "nx-table__bone nx-mail__bone", style: { width: "9rem" } }), _jsx("span", { className: "nx-table__bone", style: { width: "92%" } }), _jsx("span", { className: "nx-table__bone", style: { width: "84%" } }), _jsx("span", { className: "nx-table__bone", style: { width: "60%" } })] }));
|
|
207
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ChatId, ChatMessage, ChatSummary } from "../../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
import type { MailDomain, MailPerson, MailThreadHead, MessageSource } from "../../Interfaces/MailInterfaces.ts";
|
|
4
|
+
/** What a thread's record slot is drawn with. */
|
|
5
|
+
export interface MailDomainContext {
|
|
6
|
+
head: MailThreadHead;
|
|
7
|
+
/** Grows each time a notification arrives: a card drawn from the record reads it again when this changes. */
|
|
8
|
+
revision: number;
|
|
9
|
+
}
|
|
10
|
+
/** What arrived in a thread from someone else. */
|
|
11
|
+
export interface MailThreadChange {
|
|
12
|
+
/** `notification` when any of the lines is one: the record it is about may have changed. */
|
|
13
|
+
reason: "notification" | "reply";
|
|
14
|
+
messages: readonly ChatMessage[];
|
|
15
|
+
}
|
|
16
|
+
export interface MailThreadProps {
|
|
17
|
+
/** Where the mailbox's data comes from: `apiMessageSource()` or `localMessageSource(...)`. */
|
|
18
|
+
source: MessageSource;
|
|
19
|
+
/** The thread. */
|
|
20
|
+
threadId: ChatId;
|
|
21
|
+
/** Who is reading: their lines sit on the right. */
|
|
22
|
+
userId: ChatId;
|
|
23
|
+
/** The head the page already holds, drawn at once when the thread opens and then read again. */
|
|
24
|
+
head?: MailThreadHead;
|
|
25
|
+
/**
|
|
26
|
+
* Draws the record the thread is about, under the people. Default: its card (`DomainCard`), read again when a
|
|
27
|
+
* notification arrives, when the kind has one in `defineChatDomains` - folded to its title and figures while the thread
|
|
28
|
+
* is narrower than 480px, as its width changes, until the reader folds or unfolds it; else the kind's name and the
|
|
29
|
+
* record's words. The head scrolls with the lines.
|
|
30
|
+
*/
|
|
31
|
+
renderDomain?: (domain: MailDomain, context: MailDomainContext) => ReactNode;
|
|
32
|
+
/** Buttons at the end of the subject's line: Archive, Move to inbox. */
|
|
33
|
+
actions?: ReactNode;
|
|
34
|
+
/** The subject's heading level. Default 2. */
|
|
35
|
+
headingLevel?: 2 | 3 | 4;
|
|
36
|
+
/** Names shown on a line of people before the rest fold into "+2". Default 3. */
|
|
37
|
+
peopleShown?: number;
|
|
38
|
+
/** After lines from someone else arrive: a notification above all, whose record may have changed. */
|
|
39
|
+
onChanged?: (change: MailThreadChange) => void;
|
|
40
|
+
/** Every time the head is read: a list's row follows it. */
|
|
41
|
+
onHead?: (head: MailThreadHead) => void;
|
|
42
|
+
/** Every time the counts change, a read thread included: a list's row clears its unread count. */
|
|
43
|
+
onSummary?: (summary: ChatSummary) => void;
|
|
44
|
+
/** Lines per page. Default 30. */
|
|
45
|
+
pageSize?: number;
|
|
46
|
+
/** Milliseconds between checks for new lines. 0 turns it off. Default 15,000. */
|
|
47
|
+
pollInterval?: number;
|
|
48
|
+
/** How tall: pixels or a length. Default `min(70vh, 640px)`; `"100%"` fills what holds it. */
|
|
49
|
+
height?: number | string;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
/** A mail thread: the head with its people and record, the lines, and the reply box. */
|
|
53
|
+
export declare function MailThread({ source, threadId, userId, head: givenHead, renderDomain, actions, headingLevel, peopleShown, onChanged, onHead, onSummary, pageSize, pollInterval, height, className, }: MailThreadProps): import("react").JSX.Element;
|
|
54
|
+
export interface MailPeopleProps {
|
|
55
|
+
people: readonly MailPerson[];
|
|
56
|
+
/** The reader: their own name says "you". */
|
|
57
|
+
userId?: ChatId;
|
|
58
|
+
/** Names shown before the rest fold into "+2". Default 3. */
|
|
59
|
+
shown?: number;
|
|
60
|
+
/** What an empty list says. Default "Nobody". */
|
|
61
|
+
empty?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A list of people on one line, folded to its first names and a "+2" button that opens the rest in place: a thread sent
|
|
65
|
+
* to fifty people keeps a head of a few lines. Someone who has left keeps their name, muted.
|
|
66
|
+
*/
|
|
67
|
+
export declare function MailPeople({ people, userId, shown, empty }: MailPeopleProps): import("react").JSX.Element;
|
|
68
|
+
/** The record a thread is about, as its words alone: the kind's name as the app gave it, and the record's own words. */
|
|
69
|
+
export declare function MailDomainLine({ domain }: {
|
|
70
|
+
domain: MailDomain;
|
|
71
|
+
}): import("react").JSX.Element;
|