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,96 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ChatDomainValue, ChatId, ChatMessage, ChatSource } from "../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
import type { MailFolders, MailInboxRow, MailKindCount, MailList, MailThreadKind, MessageSource } from "../Interfaces/MailInterfaces.ts";
|
|
4
|
+
import { type ChatTimeOptions } from "./ChatHelpers.ts";
|
|
5
|
+
/** Which conversations a mail client lists: one of its folders, narrowed to one type or not. */
|
|
6
|
+
export interface MailClientView {
|
|
7
|
+
list: MailList;
|
|
8
|
+
/** The type picked under the folders: `mail`, `chat`, or `domain:<domainType>`; `null` for every type. */
|
|
9
|
+
type: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** One type of a mailbox as its rail lists it: Mail, Chat, then one per record kind. */
|
|
12
|
+
export interface MailTypeEntry {
|
|
13
|
+
/** `mail`, `chat`, or `domain:<domainType>`: the view's `type`. */
|
|
14
|
+
key: string;
|
|
15
|
+
threadKind: MailThreadKind;
|
|
16
|
+
domainType: ChatDomainValue | null;
|
|
17
|
+
label: string;
|
|
18
|
+
/** The kind's own icon, from its `defineChatDomains` row. */
|
|
19
|
+
icon?: ReactNode;
|
|
20
|
+
unread: number;
|
|
21
|
+
total: number;
|
|
22
|
+
}
|
|
23
|
+
/** How the list and the reading pane share a wide mail client: side by side, or one over the other. */
|
|
24
|
+
export type MailSplit = "columns" | "rows";
|
|
25
|
+
/** Which pane a mail client too narrow for two shows. */
|
|
26
|
+
export type MailPane = "rail" | "list" | "reader";
|
|
27
|
+
/** The key of a type: `mail`, `chat`, or `domain:<domainType>`. */
|
|
28
|
+
export declare function mailTypeKey(threadKind: MailThreadKind, domainType?: ChatDomainValue | null): string;
|
|
29
|
+
/** The kind and record kind a type's key names, for a list request. */
|
|
30
|
+
export declare function mailTypeOf(key: string | null | undefined): {
|
|
31
|
+
threadKind: MailThreadKind;
|
|
32
|
+
domainType?: string;
|
|
33
|
+
} | null;
|
|
34
|
+
/** The kind of a row: what the backend said, else mail or the record's kind. */
|
|
35
|
+
export declare function mailRowKind(row: Pick<MailInboxRow, "threadKind" | "domainType">): MailThreadKind;
|
|
36
|
+
/** The type key of a row, for its chip. */
|
|
37
|
+
export declare function mailRowTypeKey(row: Pick<MailInboxRow, "threadKind" | "domainType">): string;
|
|
38
|
+
/** A type's name: Mail, Chat, or the record kind's name as the app gave it (`defineChatDomains`). */
|
|
39
|
+
export declare function mailTypeLabel(threadKind: MailThreadKind, domainType?: ChatDomainValue | null): string;
|
|
40
|
+
/**
|
|
41
|
+
* The types a mailbox lists from its counts: Mail and Chat first, then each record kind in the order the app named
|
|
42
|
+
* them, then any it did not name. A type with nothing in it is left out; two counts of one type are added.
|
|
43
|
+
*/
|
|
44
|
+
export declare function mailTypes(kinds: readonly MailKindCount[]): MailTypeEntry[];
|
|
45
|
+
/** A folder's unread count from the counts, 0 when unknown. */
|
|
46
|
+
export declare function mailFolderUnread(folders: MailFolders | null | undefined, list: MailList): number;
|
|
47
|
+
/** The time a row shows: today's as a time ("10:42 AM"), yesterday's as "Yesterday", older as a date. */
|
|
48
|
+
export declare function mailListTime(utc: string | null | undefined, options?: ChatTimeOptions, today?: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* A thread's discussion as a chat of its own: its chat lines alone (the mails and notifications are read as mail), and
|
|
51
|
+
* whatever is written in it a chat line. The read mark is the thread's, so reading the discussion reads the thread up to
|
|
52
|
+
* its newest chat line.
|
|
53
|
+
*/
|
|
54
|
+
export declare function mailDiscussionSource(thread: ChatSource): ChatSource;
|
|
55
|
+
/**
|
|
56
|
+
* The read marks a mail client has on their way. A conversation shows read as soon as it opens, before its read mark
|
|
57
|
+
* reaches the server, so counts read at that moment would still hold it unread: the client reads its counts once
|
|
58
|
+
* `pending` is empty, and reads them again when a mark is answered (`onSettled`).
|
|
59
|
+
*/
|
|
60
|
+
export interface MailReadMarks {
|
|
61
|
+
/** The marks on their way, each settling (never failing) when the server answered or did not. */
|
|
62
|
+
readonly pending: Set<Promise<void>>;
|
|
63
|
+
/** After each mark, answered or failed. */
|
|
64
|
+
onSettled: () => void;
|
|
65
|
+
}
|
|
66
|
+
/** A conversation's lines whose read marks are followed in `marks`. A source without read marks is given back as it is. */
|
|
67
|
+
export declare function followMailReadMarks(lines: ChatSource, marks: MailReadMarks): ChatSource;
|
|
68
|
+
/** A mailbox whose conversations' read marks are followed in `marks` (`followMailReadMarks`), for a mail client's reader and chat windows. */
|
|
69
|
+
export declare function followMailboxReadMarks(source: MessageSource, marks: MailReadMarks): MessageSource;
|
|
70
|
+
/** How many of a thread's lines are its discussion: its chat lines. */
|
|
71
|
+
export declare function mailDiscussionCount(messages: readonly ChatMessage[]): number;
|
|
72
|
+
/** The split a viewer chose for a wide mail client, `columns` until they choose. */
|
|
73
|
+
export declare function readMailSplit(viewer: ChatId): MailSplit;
|
|
74
|
+
/** Keeps a viewer's split for the next visit. */
|
|
75
|
+
export declare function writeMailSplit(viewer: ChatId, split: MailSplit): void;
|
|
76
|
+
/** What a compose window holds while it is not sent: the people by id, the subject and the Markdown body. */
|
|
77
|
+
export interface MailComposeDraft {
|
|
78
|
+
to: ChatId[];
|
|
79
|
+
cc: ChatId[];
|
|
80
|
+
bcc: ChatId[];
|
|
81
|
+
subject: string;
|
|
82
|
+
body: string;
|
|
83
|
+
/** When it was last kept (milliseconds since 1970). */
|
|
84
|
+
savedAt: number;
|
|
85
|
+
}
|
|
86
|
+
/** An empty draft. */
|
|
87
|
+
export declare function emptyMailDraft(): MailComposeDraft;
|
|
88
|
+
/** Whether a draft holds anything worth asking about: words in the subject or the body. People alone are not. */
|
|
89
|
+
export declare function mailDraftHasText(draft: Pick<MailComposeDraft, "subject" | "body"> | null | undefined): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* A viewer's unsent draft of one compose window (`new`, or `reply:<thread>`), or `null`. A draft kept by an older
|
|
92
|
+
* version, or broken by hand, reads as none.
|
|
93
|
+
*/
|
|
94
|
+
export declare function readMailDraft(viewer: ChatId, key: string): MailComposeDraft | null;
|
|
95
|
+
/** Keeps a viewer's draft of one compose window, or forgets it (`null`, or a draft with nothing in it). */
|
|
96
|
+
export declare function writeMailDraft(viewer: ChatId, key: string, draft: MailComposeDraft | null): void;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { getChatDomains } from "./ChatBackend.js";
|
|
2
|
+
import { chatDayLabel, chatLocalDate, chatTimeLabel } from "./ChatHelpers.js";
|
|
3
|
+
import { todayIsoDateIn } from "./DateTimeHelpers.js";
|
|
4
|
+
/** The key of a type: `mail`, `chat`, or `domain:<domainType>`. */
|
|
5
|
+
export function mailTypeKey(threadKind, domainType) {
|
|
6
|
+
return threadKind === "domain" ? `domain:${String(domainType ?? "")}` : threadKind;
|
|
7
|
+
}
|
|
8
|
+
/** The kind and record kind a type's key names, for a list request. */
|
|
9
|
+
export function mailTypeOf(key) {
|
|
10
|
+
if (!key)
|
|
11
|
+
return null;
|
|
12
|
+
if (key === "mail" || key === "chat")
|
|
13
|
+
return { threadKind: key };
|
|
14
|
+
if (key.startsWith("domain:"))
|
|
15
|
+
return { threadKind: "domain", domainType: key.slice("domain:".length) };
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
/** The kind of a row: what the backend said, else mail or the record's kind. */
|
|
19
|
+
export function mailRowKind(row) {
|
|
20
|
+
return row.threadKind ?? (row.domainType === null || row.domainType === undefined ? "mail" : "domain");
|
|
21
|
+
}
|
|
22
|
+
/** The type key of a row, for its chip. */
|
|
23
|
+
export function mailRowTypeKey(row) {
|
|
24
|
+
const kind = mailRowKind(row);
|
|
25
|
+
return mailTypeKey(kind, kind === "domain" ? row.domainType : null);
|
|
26
|
+
}
|
|
27
|
+
/** A type's name: Mail, Chat, or the record kind's name as the app gave it (`defineChatDomains`). */
|
|
28
|
+
export function mailTypeLabel(threadKind, domainType) {
|
|
29
|
+
if (threadKind === "mail")
|
|
30
|
+
return "Mail";
|
|
31
|
+
if (threadKind === "chat")
|
|
32
|
+
return "Chat";
|
|
33
|
+
const info = getChatDomains().find(one => String(one.domain) === String(domainType));
|
|
34
|
+
return info?.label ?? "Record";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The types a mailbox lists from its counts: Mail and Chat first, then each record kind in the order the app named
|
|
38
|
+
* them, then any it did not name. A type with nothing in it is left out; two counts of one type are added.
|
|
39
|
+
*/
|
|
40
|
+
export function mailTypes(kinds) {
|
|
41
|
+
const domains = getChatDomains();
|
|
42
|
+
const held = new Map();
|
|
43
|
+
for (const count of kinds) {
|
|
44
|
+
const key = mailTypeKey(count.threadKind, count.domainType);
|
|
45
|
+
const known = held.get(key);
|
|
46
|
+
if (known) {
|
|
47
|
+
known.unread += count.unread;
|
|
48
|
+
known.total += count.total;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const info = count.threadKind === "domain" ? domains.find(one => String(one.domain) === String(count.domainType)) : undefined;
|
|
52
|
+
held.set(key, {
|
|
53
|
+
key,
|
|
54
|
+
threadKind: count.threadKind,
|
|
55
|
+
domainType: count.threadKind === "domain" ? count.domainType : null,
|
|
56
|
+
label: mailTypeLabel(count.threadKind, count.domainType),
|
|
57
|
+
...(info?.icon !== undefined && { icon: info.icon }),
|
|
58
|
+
unread: count.unread,
|
|
59
|
+
total: count.total,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const rank = (entry) => {
|
|
63
|
+
if (entry.threadKind === "mail")
|
|
64
|
+
return -2;
|
|
65
|
+
if (entry.threadKind === "chat")
|
|
66
|
+
return -1;
|
|
67
|
+
const at = domains.findIndex(one => String(one.domain) === String(entry.domainType));
|
|
68
|
+
return at === -1 ? domains.length : at;
|
|
69
|
+
};
|
|
70
|
+
return [...held.values()].filter(entry => entry.total > 0).sort((a, b) => rank(a) - rank(b));
|
|
71
|
+
}
|
|
72
|
+
/** A folder's unread count from the counts, 0 when unknown. */
|
|
73
|
+
export function mailFolderUnread(folders, list) {
|
|
74
|
+
return folders?.folders.find(folder => folder.list === list)?.unread ?? 0;
|
|
75
|
+
}
|
|
76
|
+
/** The time a row shows: today's as a time ("10:42 AM"), yesterday's as "Yesterday", older as a date. */
|
|
77
|
+
export function mailListTime(utc, options = {}, today = todayIsoDateIn(options.timeZone)) {
|
|
78
|
+
if (!utc)
|
|
79
|
+
return "";
|
|
80
|
+
const day = chatLocalDate(utc, options.timeZone);
|
|
81
|
+
if (!day)
|
|
82
|
+
return "";
|
|
83
|
+
return day === today ? chatTimeLabel(utc, options) : chatDayLabel(day, options, today);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A thread's discussion as a chat of its own: its chat lines alone (the mails and notifications are read as mail), and
|
|
87
|
+
* whatever is written in it a chat line. The read mark is the thread's, so reading the discussion reads the thread up to
|
|
88
|
+
* its newest chat line.
|
|
89
|
+
*/
|
|
90
|
+
export function mailDiscussionSource(thread) {
|
|
91
|
+
const isChat = (message) => (message.kind ?? "chat") === "chat";
|
|
92
|
+
return {
|
|
93
|
+
...thread,
|
|
94
|
+
async history(request) {
|
|
95
|
+
const page = await thread.history(request);
|
|
96
|
+
// A chat line answering a mail quotes nothing: the mail is read in the pane, not in the discussion.
|
|
97
|
+
const left = new Set(page.messages.filter(message => !isChat(message)).map(message => String(message.id)));
|
|
98
|
+
const messages = page.messages.filter(isChat).map(message => (message.replyToId !== undefined && message.replyToId !== null && left.has(String(message.replyToId)) ? { ...message, replyToId: null } : message));
|
|
99
|
+
// The thread's counts are of every line; the discussion counts its own.
|
|
100
|
+
return { ...page, messages, ...(page.summary && { summary: { ...page.summary, messageCount: messages.length } }) };
|
|
101
|
+
},
|
|
102
|
+
send: (draft, signal) => thread.send({ ...draft, kind: "chat" }, signal),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** A conversation's lines whose read marks are followed in `marks`. A source without read marks is given back as it is. */
|
|
106
|
+
export function followMailReadMarks(lines, marks) {
|
|
107
|
+
const mark = lines.markRead;
|
|
108
|
+
if (!mark)
|
|
109
|
+
return lines;
|
|
110
|
+
return {
|
|
111
|
+
...lines,
|
|
112
|
+
markRead(upTo, signal) {
|
|
113
|
+
const work = mark(upTo, signal);
|
|
114
|
+
const settled = work.then(() => undefined, () => undefined);
|
|
115
|
+
marks.pending.add(settled);
|
|
116
|
+
void settled.then(() => {
|
|
117
|
+
marks.pending.delete(settled);
|
|
118
|
+
marks.onSettled();
|
|
119
|
+
});
|
|
120
|
+
return work;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** A mailbox whose conversations' read marks are followed in `marks` (`followMailReadMarks`), for a mail client's reader and chat windows. */
|
|
125
|
+
export function followMailboxReadMarks(source, marks) {
|
|
126
|
+
return { ...source, threadSource: id => followMailReadMarks(source.threadSource(id), marks) };
|
|
127
|
+
}
|
|
128
|
+
/** How many of a thread's lines are its discussion: its chat lines. */
|
|
129
|
+
export function mailDiscussionCount(messages) {
|
|
130
|
+
return messages.filter(message => (message.kind ?? "chat") === "chat" && message.status !== "failed").length;
|
|
131
|
+
}
|
|
132
|
+
// Remembered in this browser, per viewer
|
|
133
|
+
const SPLIT_PREFIX = "nx-mail-split:";
|
|
134
|
+
const DRAFT_PREFIX = "nx-mail-draft:";
|
|
135
|
+
function storage() {
|
|
136
|
+
try {
|
|
137
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function read(key) {
|
|
144
|
+
try {
|
|
145
|
+
return storage()?.getItem(key) ?? null;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function write(key, value) {
|
|
152
|
+
try {
|
|
153
|
+
const store = storage();
|
|
154
|
+
if (!store)
|
|
155
|
+
return;
|
|
156
|
+
if (value === null)
|
|
157
|
+
store.removeItem(key);
|
|
158
|
+
else
|
|
159
|
+
store.setItem(key, value);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// A private window or a full disk: the preference is simply not kept.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** The split a viewer chose for a wide mail client, `columns` until they choose. */
|
|
166
|
+
export function readMailSplit(viewer) {
|
|
167
|
+
return read(SPLIT_PREFIX + String(viewer)) === "rows" ? "rows" : "columns";
|
|
168
|
+
}
|
|
169
|
+
/** Keeps a viewer's split for the next visit. */
|
|
170
|
+
export function writeMailSplit(viewer, split) {
|
|
171
|
+
write(SPLIT_PREFIX + String(viewer), split);
|
|
172
|
+
}
|
|
173
|
+
/** An empty draft. */
|
|
174
|
+
export function emptyMailDraft() {
|
|
175
|
+
return { to: [], cc: [], bcc: [], subject: "", body: "", savedAt: 0 };
|
|
176
|
+
}
|
|
177
|
+
/** Whether a draft holds anything worth asking about: words in the subject or the body. People alone are not. */
|
|
178
|
+
export function mailDraftHasText(draft) {
|
|
179
|
+
return !!draft && (draft.subject.trim() !== "" || draft.body.trim() !== "");
|
|
180
|
+
}
|
|
181
|
+
const ids = (value) => (Array.isArray(value) ? value.filter((one) => typeof one === "string" || typeof one === "number") : []);
|
|
182
|
+
/**
|
|
183
|
+
* A viewer's unsent draft of one compose window (`new`, or `reply:<thread>`), or `null`. A draft kept by an older
|
|
184
|
+
* version, or broken by hand, reads as none.
|
|
185
|
+
*/
|
|
186
|
+
export function readMailDraft(viewer, key) {
|
|
187
|
+
const text = read(`${DRAFT_PREFIX}${String(viewer)}:${key}`);
|
|
188
|
+
if (!text)
|
|
189
|
+
return null;
|
|
190
|
+
try {
|
|
191
|
+
const value = JSON.parse(text);
|
|
192
|
+
if (typeof value !== "object" || value === null)
|
|
193
|
+
return null;
|
|
194
|
+
return {
|
|
195
|
+
to: ids(value.to),
|
|
196
|
+
cc: ids(value.cc),
|
|
197
|
+
bcc: ids(value.bcc),
|
|
198
|
+
subject: typeof value.subject === "string" ? value.subject : "",
|
|
199
|
+
body: typeof value.body === "string" ? value.body : "",
|
|
200
|
+
savedAt: typeof value.savedAt === "number" ? value.savedAt : 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Keeps a viewer's draft of one compose window, or forgets it (`null`, or a draft with nothing in it). */
|
|
208
|
+
export function writeMailDraft(viewer, key, draft) {
|
|
209
|
+
const empty = !draft || (!mailDraftHasText(draft) && draft.to.length === 0 && draft.cc.length === 0 && draft.bcc.length === 0);
|
|
210
|
+
write(`${DRAFT_PREFIX}${String(viewer)}:${key}`, empty ? null : JSON.stringify(draft));
|
|
211
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type ApiEndpoint, type ApiModuleValue } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { ChatDomainValue, ChatDraft, ChatHistory, ChatHistoryRequest, ChatId, ChatMessage } from "../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
import type { DropdownPage, DropdownPageRequest } from "../Interfaces/InputInterfaces.ts";
|
|
4
|
+
import type { MailDefaults, MailDomain, MailDraft, MailFolders, MailHeadKind, MailInboxRequest, MailInboxRow, MailPerson, MailRole, MailSent, MailThreadHead, MailThreadKind, MailUnread } from "../Interfaces/MailInterfaces.ts";
|
|
5
|
+
import type { TablePage } from "../Interfaces/TableInterfaces.ts";
|
|
6
|
+
import { NexusChatBackend } from "./ChatBackend.ts";
|
|
7
|
+
/** The actions a mailbox needs from a backend. */
|
|
8
|
+
export declare const MAIL_ACTIONS: {
|
|
9
|
+
/** A page of one list: inbox, unread, sent, archived. */
|
|
10
|
+
readonly inbox: "Inbox";
|
|
11
|
+
/** A thread's head: its subject and its people. */
|
|
12
|
+
readonly thread: "Thread";
|
|
13
|
+
/** A page of a thread's lines. */
|
|
14
|
+
readonly history: "History";
|
|
15
|
+
/** A new message. */
|
|
16
|
+
readonly compose: "Create";
|
|
17
|
+
/** A reply in a thread. */
|
|
18
|
+
readonly reply: "Reply";
|
|
19
|
+
/** How far the reader has read a thread. */
|
|
20
|
+
readonly read: "Read";
|
|
21
|
+
/** Threads put aside. */
|
|
22
|
+
readonly archive: "Archive";
|
|
23
|
+
/** Threads brought back. */
|
|
24
|
+
readonly unarchive: "Unarchive";
|
|
25
|
+
/** How much is unread, and the newest of it. */
|
|
26
|
+
readonly unread: "Unread";
|
|
27
|
+
/** The people a message can be sent to. */
|
|
28
|
+
readonly people: "People";
|
|
29
|
+
/** Who a new message goes to by default. */
|
|
30
|
+
readonly defaults: "Defaults";
|
|
31
|
+
/** The counts beside a mail client's folders and types. */
|
|
32
|
+
readonly folders: "Folders";
|
|
33
|
+
/** Whole conversations marked read. */
|
|
34
|
+
readonly markRead: "MarkRead";
|
|
35
|
+
/** Conversations marked unread again. */
|
|
36
|
+
readonly markUnread: "MarkUnread";
|
|
37
|
+
/** Conversations starred. */
|
|
38
|
+
readonly star: "Star";
|
|
39
|
+
/** Stars taken away. */
|
|
40
|
+
readonly unstar: "Unstar";
|
|
41
|
+
};
|
|
42
|
+
export type MailAction = keyof typeof MAIL_ACTIONS;
|
|
43
|
+
type Row = Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* What a mailbox needs from a backend. Extend `NexusMessageBackend` for an app whose mail API differs, and register it
|
|
46
|
+
* once beside the app's API modules:
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* class AppMessageBackend extends NexusMessageBackend {
|
|
50
|
+
* override readonly module = AppModule.Messages;
|
|
51
|
+
* }
|
|
52
|
+
* defineMessageBackend(new AppMessageBackend());
|
|
53
|
+
*/
|
|
54
|
+
export declare abstract class MessageBackend {
|
|
55
|
+
/** Which endpoint serves an action, in one of the app's own modules. */
|
|
56
|
+
abstract endpoint(action: MailAction): ApiEndpoint;
|
|
57
|
+
/** What asking for a page of a list sends. */
|
|
58
|
+
abstract inboxRequest(request: MailInboxRequest): unknown;
|
|
59
|
+
/** Reads a page of a list. */
|
|
60
|
+
abstract readInbox(result: unknown): TablePage<MailInboxRow>;
|
|
61
|
+
/** Reads one row of a list: also what a table of raw rows (a CRUD on the same endpoint) reads a row by. */
|
|
62
|
+
abstract readInboxRow(row: unknown): MailInboxRow;
|
|
63
|
+
/** What opening a thread sends, as the call's `params`: its id in the path. */
|
|
64
|
+
threadParams(threadId: ChatId): Record<string, string | number>;
|
|
65
|
+
/** Reads a thread's head. */
|
|
66
|
+
abstract readThread(result: unknown): MailThreadHead;
|
|
67
|
+
/** What a page of a thread's lines sends. */
|
|
68
|
+
abstract historyRequest(threadId: ChatId, request: ChatHistoryRequest): unknown;
|
|
69
|
+
/** Reads a page of a thread's lines, as a chat's history. */
|
|
70
|
+
abstract readHistory(result: unknown): ChatHistory;
|
|
71
|
+
/** What a reply sends. */
|
|
72
|
+
abstract replyRequest(threadId: ChatId, draft: ChatDraft): unknown;
|
|
73
|
+
/** Reads the reply saved. The draft is there for a backend that answers with an id alone. */
|
|
74
|
+
abstract readReply(result: unknown, draft: ChatDraft): ChatMessage;
|
|
75
|
+
/** What the read mark sends. */
|
|
76
|
+
abstract readMarkRequest(threadId: ChatId, upTo: ChatId): unknown;
|
|
77
|
+
/** What a new message sends. */
|
|
78
|
+
abstract composeRequest(draft: MailDraft): unknown;
|
|
79
|
+
/** Reads the thread a new message made. */
|
|
80
|
+
abstract readComposed(result: unknown): MailSent;
|
|
81
|
+
/** What Archive and Unarchive send. */
|
|
82
|
+
abstract markRequest(ids: readonly ChatId[]): unknown;
|
|
83
|
+
/** Reads how many threads Archive or Unarchive changed. */
|
|
84
|
+
abstract readChanged(result: unknown): number;
|
|
85
|
+
/** Reads what is unread. */
|
|
86
|
+
abstract readUnread(result: unknown): MailUnread;
|
|
87
|
+
/** What a search for people sends. */
|
|
88
|
+
abstract peopleRequest(query: DropdownPageRequest): unknown;
|
|
89
|
+
/** Reads a page of people. */
|
|
90
|
+
abstract readPeople(result: unknown): DropdownPage<MailPerson>;
|
|
91
|
+
/** Reads who a new message goes to by default. */
|
|
92
|
+
abstract readDefaults(result: unknown): MailDefaults;
|
|
93
|
+
/**
|
|
94
|
+
* Reads the counts beside a mail client's folders and types. A backend without the `folders` door leaves it out, and
|
|
95
|
+
* the client shows no counts and no Types. So are `markRead`, `markUnread` and `star` / `unstar` optional: their
|
|
96
|
+
* doors send `markRequest` and are read by `readChanged`; a backend without them sets `supports` to false for each.
|
|
97
|
+
*/
|
|
98
|
+
readFolders?(result: unknown): MailFolders;
|
|
99
|
+
/** The optional doors this backend has: a mail client offers Mark read, Mark unread and stars only where they are. */
|
|
100
|
+
supports(action: "folders" | "markRead" | "markUnread" | "star"): boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The Nexus Message module's `Mail` controller (`App/Api/Mail/...`): POST `Inbox`, `History`, `Create`, `Reply` and
|
|
104
|
+
* `People`; GET `Thread/{id}`, `Unread` and `Defaults`; PUT `Read`, `Archive` and `Unarchive`. Pages answer
|
|
105
|
+
* `{ results, totalCount, filterCount }`; a person's role and a message's kind come as the backend's numbers and are
|
|
106
|
+
* read as this package's words. Override `module` or `controller` for another place, or any reader for another shape.
|
|
107
|
+
*/
|
|
108
|
+
export declare class NexusMessageBackend extends MessageBackend {
|
|
109
|
+
/** The module the mail endpoints live in. `NexusModule.Message`. */
|
|
110
|
+
readonly module: ApiModuleValue;
|
|
111
|
+
/** The controller the actions hang off. */
|
|
112
|
+
readonly controller: string;
|
|
113
|
+
/** A thread's lines are the chat's rows, read as the chat reads them. */
|
|
114
|
+
protected readonly chat: NexusChatBackend<ChatDomainValue>;
|
|
115
|
+
endpoint(action: MailAction): ApiEndpoint;
|
|
116
|
+
inboxRequest(request: MailInboxRequest): unknown;
|
|
117
|
+
readInbox(result: unknown): TablePage<MailInboxRow>;
|
|
118
|
+
readInboxRow(row: unknown): MailInboxRow;
|
|
119
|
+
readThread(result: unknown): MailThreadHead;
|
|
120
|
+
historyRequest(threadId: ChatId, request: ChatHistoryRequest): unknown;
|
|
121
|
+
readHistory(result: unknown): ChatHistory;
|
|
122
|
+
replyRequest(threadId: ChatId, draft: ChatDraft): unknown;
|
|
123
|
+
readReply(result: unknown, draft: ChatDraft): ChatMessage;
|
|
124
|
+
readMarkRequest(threadId: ChatId, upTo: ChatId): unknown;
|
|
125
|
+
composeRequest(draft: MailDraft): unknown;
|
|
126
|
+
readComposed(result: unknown): MailSent;
|
|
127
|
+
markRequest(ids: readonly ChatId[]): unknown;
|
|
128
|
+
readChanged(result: unknown): number;
|
|
129
|
+
readUnread(result: unknown): MailUnread;
|
|
130
|
+
peopleRequest(query: DropdownPageRequest): unknown;
|
|
131
|
+
readPeople(result: unknown): DropdownPage<MailPerson>;
|
|
132
|
+
readDefaults(result: unknown): MailDefaults;
|
|
133
|
+
readFolders(result: unknown): MailFolders;
|
|
134
|
+
supports(action: "folders" | "markRead" | "markUnread" | "star"): boolean;
|
|
135
|
+
/** A conversation's kind: the wire's number (Mail 1, Chat 2, Domain 3) or a word; without one, mail or its record's kind. */
|
|
136
|
+
protected toThreadKind(value: unknown, domainType: ChatDomainValue | null): MailThreadKind;
|
|
137
|
+
/** A person. */
|
|
138
|
+
protected toPerson(row: Row): MailPerson;
|
|
139
|
+
/** A role, from the backend's `EChatParticipantRoles` (1 From, 2 To, 3 Cc, 4 Bcc) or a word. A plain member reads as To. */
|
|
140
|
+
protected toRole(value: unknown): MailRole;
|
|
141
|
+
/** What a thread opened with: the backend's `EChatMessageTypes` (1 Mail, 2 Notification) or a word. */
|
|
142
|
+
protected toHeadKind(value: unknown): MailHeadKind;
|
|
143
|
+
/** A domain type is the app's own value: kept as it came, a number or a word, and none when it is none. */
|
|
144
|
+
protected toDomainType(value: unknown): ChatDomainValue | null;
|
|
145
|
+
/** The record a thread is about. */
|
|
146
|
+
protected toDomain(row: Row): MailDomain | null;
|
|
147
|
+
}
|
|
148
|
+
/** Makes an app's mail backend the one every mailbox uses, and returns it. Call it once, beside `defineApiModules`. */
|
|
149
|
+
export declare function defineMessageBackend<T extends MessageBackend>(backend: T): T;
|
|
150
|
+
/** The app's mail backend: the one given to `defineMessageBackend`, else the Nexus one. */
|
|
151
|
+
export declare function getMessageBackend(): MessageBackend;
|
|
152
|
+
export {};
|