nexus-shared 3.2.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.
Files changed (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. package/src/Styles/Nexus.Tree.css +41 -1
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconArchive, InlineIcon } from "nexus-icons";
4
+ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { flushSync } from "react-dom";
6
+ import { chatDomainLabel } from "../../Helpers/ChatBackend.js";
7
+ import { chatDayLabel, chatLocalDate, chatTimeLabel, sameChatId } from "../../Helpers/ChatHelpers.js";
8
+ import { useChat } from "../../Helpers/ChatHooks.js";
9
+ import { cx } from "../../Helpers/ClassHelpers.js";
10
+ import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
11
+ import { formatDateTime } from "../../Helpers/DateTimeHelpers.js";
12
+ import { mailBccNote, mailPeopleLine, MAIL_ROLE_LABELS } from "../../Helpers/MessageHelpers.js";
13
+ import { ChatComposer } from "../Chats/ChatComposer.js";
14
+ import { ChatFailure } from "../Chats/ChatParts.js";
15
+ import { ChatThread } from "../Chats/ChatThread.js";
16
+ import { DomainCard, DomainLine } from "../Domains/DomainCard.js";
17
+ /** Under this width (px) a record's card opens folded to its title and figures, the details a press away. */
18
+ const NARROW_THREAD = 480;
19
+ /** The words of a thread the reader may not open: a stranger learns nothing more. */
20
+ const REFUSED = "This message is not available to you.";
21
+ const errorStatus = (error) => {
22
+ const status = error?.status;
23
+ return typeof status === "number" ? status : undefined;
24
+ };
25
+ const isAbort = (error) => error?.name === "AbortError";
26
+ /** A mail thread: the head with its people and record, the lines, and the reply box. */
27
+ export function MailThread({ source, threadId, userId, head: givenHead, renderDomain, actions, headingLevel = 2, peopleShown = 3, onChanged, onHead, onSummary, pageSize, pollInterval, height, className, }) {
28
+ const [head, setHead] = useState(givenHead && sameChatId(givenHead.id, threadId) ? givenHead : null);
29
+ const [headState, setHeadState] = useState({ status: head ? "ready" : "loading" });
30
+ const [attempt, setAttempt] = useState(0);
31
+ const [revision, setRevision] = useState(0);
32
+ const [replyTo, setReplyTo] = useState(null);
33
+ const latest = useRef({ onChanged, onHead, onSummary, head });
34
+ // How wide the thread is, followed as it changes. A window still laying out reads 0 (its dialog is not open yet), so the
35
+ // record's card waits for a real width before it is drawn - the first one drawn before the browser paints - and then
36
+ // folds or unfolds with the width, until the reader folds or unfolds it themselves.
37
+ const frame = useRef(null);
38
+ const [narrow, setNarrow] = useState(null);
39
+ const [folded, setFolded] = useState(null);
40
+ useLayoutEffect(() => {
41
+ const element = frame.current;
42
+ if (!element)
43
+ return;
44
+ let known = false;
45
+ const read = () => {
46
+ const width = element.clientWidth;
47
+ if (width <= 0)
48
+ return;
49
+ const next = width < NARROW_THREAD;
50
+ if (known)
51
+ setNarrow(next);
52
+ else
53
+ flushSync(() => setNarrow(next));
54
+ known = true;
55
+ };
56
+ const width = element.clientWidth;
57
+ if (width > 0) {
58
+ setNarrow(width < NARROW_THREAD);
59
+ known = true;
60
+ }
61
+ const observer = new ResizeObserver(read);
62
+ observer.observe(element);
63
+ return () => observer.disconnect();
64
+ }, []);
65
+ latest.current = { onChanged, onHead, onSummary, head };
66
+ const chatSource = useMemo(() => source.threadSource(threadId), [source, threadId]);
67
+ // A mail thread is not a record's place: the scope only keys the store, and the conversation id keeps the page's
68
+ // count cache (the chat icons' counts) out of it.
69
+ const scope = useMemo(() => ({ domain: "mail", parentId: threadId, userId }), [threadId, userId]);
70
+ const refused = headState.status === "refused";
71
+ const { store, state } = useChat({ scope, conversationId: threadId, source: chatSource, pageSize, pollInterval, enabled: !refused });
72
+ const take = useCallback((next) => {
73
+ setHead(next);
74
+ setHeadState({ status: "ready" });
75
+ latest.current.onHead?.(next);
76
+ }, []);
77
+ // The head, read whenever the thread changes, and again on Retry.
78
+ useEffect(() => {
79
+ const controller = new AbortController();
80
+ if (!latest.current.head || !sameChatId(latest.current.head.id, threadId)) {
81
+ setHead(null);
82
+ setHeadState({ status: "loading" });
83
+ }
84
+ source.thread(threadId, controller.signal).then(take, error => {
85
+ if (controller.signal.aborted || isAbort(error))
86
+ return;
87
+ const status = errorStatus(error);
88
+ if (status === 403 || status === 404)
89
+ setHeadState({ status: "refused", message: REFUSED });
90
+ else
91
+ setHeadState({ status: "failed", message: error instanceof Error && error.message ? error.message : "The message could not be loaded." });
92
+ });
93
+ return () => controller.abort();
94
+ }, [source, threadId, attempt, take]);
95
+ // Read again quietly when the people may have changed: the head on screen stays until the new one lands.
96
+ const refreshHead = useCallback(() => {
97
+ source.thread(threadId).then(take, () => {
98
+ // A refresh that failed leaves the head as it was; the next line tries again.
99
+ });
100
+ }, [source, threadId, take]);
101
+ // What arrived since the last look: someone else's lines read the head again and tell the page; a reply of this
102
+ // reader's while copied unseen reads it again too, since it made them a Cc.
103
+ const primed = useRef(false);
104
+ const newest = useRef(null);
105
+ useEffect(() => {
106
+ // Another thread starts its own count: its first page is what it holds, not what arrived.
107
+ primed.current = false;
108
+ newest.current = null;
109
+ }, [threadId]);
110
+ useEffect(() => {
111
+ if (!state.loaded)
112
+ return;
113
+ const saved = state.messages.filter(message => message.status !== "sending" && message.status !== "failed");
114
+ const last = saved[saved.length - 1];
115
+ const key = last ? String(last.id) : null;
116
+ const before = newest.current;
117
+ newest.current = key;
118
+ if (!primed.current) {
119
+ primed.current = true;
120
+ return;
121
+ }
122
+ if (key === before)
123
+ return;
124
+ const from = before === null ? -1 : saved.findIndex(message => String(message.id) === before);
125
+ const fresh = saved.slice(from + 1);
126
+ const theirs = fresh.filter(message => !sameChatId(message.authorId, userId));
127
+ const mineAsBcc = latest.current.head?.myRole === "bcc" && fresh.some(message => sameChatId(message.authorId, userId));
128
+ if (theirs.length > 0 || mineAsBcc)
129
+ refreshHead();
130
+ if (theirs.length === 0)
131
+ return;
132
+ const notification = theirs.some(message => message.kind === "notification");
133
+ if (notification)
134
+ setRevision(count => count + 1);
135
+ latest.current.onChanged?.({ reason: notification ? "notification" : "reply", messages: theirs });
136
+ }, [state.loaded, state.messages, userId, refreshHead]);
137
+ useEffect(() => {
138
+ latest.current.onSummary?.(state.summary);
139
+ }, [state.summary]);
140
+ const preferences = useDatePreferences();
141
+ const zone = useBrowserTimeZone();
142
+ const time = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }) }), [preferences, zone]);
143
+ const style = height === undefined ? undefined : { "--nx-chat-height": typeof height === "number" ? `${height}px` : height };
144
+ const Heading = `h${headingLevel}`;
145
+ const note = mailBccNote(head);
146
+ if (refused) {
147
+ return (_jsx("div", { className: cx("nx-chat-panel", "nx-mail", className), style: style, children: _jsx("div", { className: "nx-mail__refused", role: "alert", children: _jsx("p", { children: headState.message }) }) }));
148
+ }
149
+ // The head opens the thread's one scroll (its lead): a tall record's card is read by scrolling the thread, not in a box.
150
+ const headBand = (_jsx("header", { className: "nx-mail__head", "aria-busy": headState.status === "loading" || undefined, children: headState.status === "failed" && !head ? (_jsx(ChatFailure, { message: headState.message, onRetry: () => setAttempt(count => count + 1) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "nx-mail__title", children: [_jsx(Heading, { className: "nx-mail__subject", tabIndex: -1, "data-nx-focus": "", children: head ? head.subject : _jsx("span", { className: "nx-table__bone nx-record__bone nx-mail__bone", style: { width: "16rem" } }) }), head?.archived && (_jsxs("span", { className: "nx-table__tag nx-table__tag--plain nx-mail__archived", children: [_jsx(InlineIcon, { icon: iconArchive }), " Archived"] })), head && _jsx(MailTime, { value: head.createdTime, time: time }), actions && _jsx("span", { className: "nx-mail__actions", children: actions })] }), _jsx("dl", { className: "nx-mail__people", children: head ? (_jsxs(_Fragment, { children: [_jsx(MailPeopleRow, { role: "from", people: [head.from], userId: userId, shown: peopleShown }), _jsx(MailPeopleRow, { role: "to", people: head.to, userId: userId, shown: peopleShown }), head.cc.length > 0 && _jsx(MailPeopleRow, { role: "cc", people: head.cc, userId: userId, shown: peopleShown }), head.bcc.length > 0 && _jsx(MailPeopleRow, { role: "bcc", people: head.bcc, userId: userId, shown: peopleShown })] })) : (_jsxs(_Fragment, { children: [_jsx(MailBoneRow, { label: MAIL_ROLE_LABELS.from, width: "9rem" }), _jsx(MailBoneRow, { label: MAIL_ROLE_LABELS.to, width: "13rem" })] })) }), head?.domain &&
151
+ (renderDomain ? (renderDomain(head.domain, { head, revision })) : (narrow !== null && (_jsx(DomainCard, { domain: head.domain.domainType, id: head.domain.id, label: head.domain.label, revision: revision, viewer: userId, headingLevel: (headingLevel + 1), collapsed: folded ?? narrow, onCollapsedChange: setFolded })))), headState.status === "failed" && head && _jsx(ChatFailure, { message: headState.message, onRetry: () => setAttempt(count => count + 1) })] })) }));
152
+ return (_jsxs("div", { ref: frame, className: cx("nx-chat-panel", "nx-mail", className), style: style, children: [_jsx("div", { className: "nx-chat-panel__body", children: _jsx(ChatThread, { store: store, state: state, userId: userId, onReply: setReplyTo, emptyMessage: "Nothing in this thread yet.", lead: headBand }) }), _jsx(ChatComposer, { store: store, state: state, draftKey: `mail:${String(threadId)}:${String(userId)}`, placeholder: "Write a reply\u2026", replyTo: replyTo, onCancelReply: () => setReplyTo(null), notice: note, readOnlyMessage: "This thread is closed: it can be read, not answered." })] }));
153
+ }
154
+ /** When the thread was started: the day and the time in the reader's settings, the full moment on hover. */
155
+ function MailTime({ value, time }) {
156
+ if (!value)
157
+ return null;
158
+ const day = chatLocalDate(value, time.timeZone);
159
+ return (_jsx("time", { className: "nx-mail__time", dateTime: value, title: formatDateTime(value, time), children: day ? `${chatDayLabel(day, time)} · ${chatTimeLabel(value, time)}` : chatTimeLabel(value, time) }));
160
+ }
161
+ /** A line of the head held while it loads: the role's word and a bone where the names go. */
162
+ function MailBoneRow({ label, width }) {
163
+ return (_jsxs("div", { className: "nx-mail__row", children: [_jsx("dt", { children: label }), _jsx("dd", { children: _jsx("span", { className: "nx-table__bone nx-record__bone nx-mail__bone", style: { width } }) })] }));
164
+ }
165
+ /** One line of the head: a role and its people, folded to a few names and "+2" until it is opened. */
166
+ function MailPeopleRow({ role, people, userId, shown }) {
167
+ return (_jsxs("div", { className: "nx-mail__row", "data-role": role, children: [_jsx("dt", { children: MAIL_ROLE_LABELS[role] }), _jsx("dd", { children: _jsx(MailPeople, { people: people, userId: userId, shown: shown }) })] }));
168
+ }
169
+ /**
170
+ * 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
171
+ * to fifty people keeps a head of a few lines. Someone who has left keeps their name, muted.
172
+ */
173
+ export function MailPeople({ people, userId, shown = 3, empty = "Nobody" }) {
174
+ const [open, setOpen] = useState(false);
175
+ const listId = useId();
176
+ const line = mailPeopleLine(people, shown);
177
+ if (people.length === 0)
178
+ return _jsx("span", { className: "nx-mail__nobody", children: empty });
179
+ const visible = open ? people : line.shown;
180
+ return (_jsxs("span", { id: listId, children: [visible.map((person, index) => (_jsxs("span", { className: cx("nx-mail__name", person.isActive === false && "nx-mail__name--inactive"), title: person.isActive === false ? `${person.name} has left` : undefined, children: [person.name, userId !== undefined && sameChatId(person.id, userId) && _jsx("span", { className: "nx-mail__you", children: "you" }), index < visible.length - 1 && ","] }, `${String(person.id)}-${index}`))), line.more > 0 && (_jsx("button", { type: "button", className: "nx-mail__more", "aria-expanded": open, "aria-controls": listId, "aria-label": open ? "Show fewer" : `Show ${line.more} more: ${people.slice(line.shown.length).map(person => person.name).join(", ")}`, onClick: () => setOpen(was => !was), children: open ? "Fewer" : `+${line.more}` }))] }));
181
+ }
182
+ /** 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. */
183
+ export function MailDomainLine({ domain }) {
184
+ return _jsx(DomainLine, { kind: chatDomainLabel(domain.domainType), label: domain.label });
185
+ }
@@ -0,0 +1,41 @@
1
+ import { type IconNode } from "nexus-icons";
2
+ import { type ChatTimeOptions } from "../../Helpers/ChatHelpers.ts";
3
+ import { type MessageUnreadSnapshot } from "../../Helpers/MessageUnread.ts";
4
+ import type { MailInboxRow, MessageSource } from "../../Interfaces/MailInterfaces.ts";
5
+ export interface UseMessageUnreadOptions {
6
+ /** Milliseconds between reads while the page is in front. Default a minute. */
7
+ interval?: number;
8
+ /** Joins the page's reads. False shows what is known without reading. Default true. */
9
+ enabled?: boolean;
10
+ }
11
+ /** What `useMessageUnread` gives: the count and the newest, `refresh()`, and whether this one speaks for the page. */
12
+ export interface MessageUnreadHandle extends MessageUnreadSnapshot {
13
+ refresh: () => void;
14
+ /** The first drawn of the page's bells on this source: the one with the live region. */
15
+ speaks: boolean;
16
+ }
17
+ /** The page's unread count for a mailbox, shared by every component that asks: one read however many there are. */
18
+ export declare function useMessageUnread(source: Pick<MessageSource, "unread">, { interval, enabled }?: UseMessageUnreadOptions): MessageUnreadHandle;
19
+ export interface MessageBellProps {
20
+ /** The mailbox: `apiMessageSource()`, or anything with `unread()`. One source object per page shares one poller. */
21
+ source: Pick<MessageSource, "unread">;
22
+ /** The inbox. A thread opens at `href?thread=<id>`. */
23
+ href: string;
24
+ /** A thread's address, instead of `href?thread=<id>`. */
25
+ threadHref?: (row: MailInboxRow) => string;
26
+ /** What the button is called, before its count. Default "Messages". */
27
+ label?: string;
28
+ /** Milliseconds between reads while the page is in front. Default a minute. */
29
+ interval?: number;
30
+ /** Default `md`, the bar's size. */
31
+ size?: "sm" | "md";
32
+ /** Default a bell. */
33
+ icon?: IconNode;
34
+ /** The largest count the badge writes before "99+". Default 99. */
35
+ maxBadge?: number;
36
+ /** The reader's date settings, for "14 min ago". Default their own. */
37
+ time?: ChatTimeOptions;
38
+ className?: string;
39
+ }
40
+ /** The unread count on a top bar's button, and the newest unread a click away. See the notes at the top of this file. */
41
+ export declare function MessageBell({ source, href, threadHref, label, interval, size, icon, maxBadge, time, className }: MessageBellProps): import("react").JSX.Element;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconBell, InlineIcon } from "nexus-icons";
4
+ import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
5
+ import { chatTimeAgo } from "../../Helpers/ChatHelpers.js";
6
+ import { cx } from "../../Helpers/ClassHelpers.js";
7
+ import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
8
+ import { MESSAGE_UNREAD_INTERVAL, mailThreadHref, mailUnreadBadge, mailUnreadWords, messageUnreadPoller, } from "../../Helpers/MessageUnread.js";
9
+ import { Button } from "../Documents/Button.js";
10
+ import { Menu } from "../Documents/Menu.js";
11
+ /** The page's unread count for a mailbox, shared by every component that asks: one read however many there are. */
12
+ export function useMessageUnread(source, { interval = MESSAGE_UNREAD_INTERVAL, enabled = true } = {}) {
13
+ const poller = messageUnreadPoller(source);
14
+ const snapshot = useSyncExternalStore(poller.subscribe, poller.getSnapshot, poller.getSnapshot);
15
+ const member = useRef(null);
16
+ useEffect(() => {
17
+ if (!enabled)
18
+ return;
19
+ const joined = poller.join(interval);
20
+ member.current = joined;
21
+ return () => {
22
+ joined.leave();
23
+ if (member.current === joined)
24
+ member.current = null;
25
+ };
26
+ }, [poller, interval, enabled]);
27
+ return { ...snapshot, refresh: () => poller.refresh(), speaks: member.current?.speaks() ?? false };
28
+ }
29
+ /** What the button says it holds: its label and the count in words, or why there is none. */
30
+ function describe(label, unread, max) {
31
+ if (unread.status === "idle" || unread.status === "loading")
32
+ return `${label}: not checked yet`;
33
+ if (unread.status === "failed")
34
+ return `${label}: could not be checked`;
35
+ return `${label}: ${mailUnreadWords(unread.count, max).toLowerCase()}`;
36
+ }
37
+ /** The unread count on a top bar's button, and the newest unread a click away. See the notes at the top of this file. */
38
+ export function MessageBell({ source, href, threadHref, label = "Messages", interval, size = "md", icon = iconBell, maxBadge = 99, time, className }) {
39
+ const unread = useMessageUnread(source, { interval });
40
+ const preferences = useDatePreferences();
41
+ const zone = useBrowserTimeZone();
42
+ const settings = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }), ...time }), [preferences, zone, time]);
43
+ const words = describe(label, unread, maxBadge);
44
+ const badge = unread.status === "ready" ? mailUnreadBadge(unread.count, maxBadge) : "";
45
+ const shown = unread.newest.reduce((total, row) => total + Math.max(0, row.unreadCount), 0);
46
+ const heading = unread.status === "idle" || unread.status === "loading"
47
+ ? "Checking for messages"
48
+ : unread.status === "failed"
49
+ ? "Could not check for messages"
50
+ : unread.count > 0
51
+ ? "Unread"
52
+ : "Nothing unread";
53
+ const items = [
54
+ { heading },
55
+ ...unread.newest.map(row => ({
56
+ value: `thread:${String(row.id)}`,
57
+ label: row.subject,
58
+ // Who wrote the newest line and when: the line that made it unread (the thread's sender wrote the first).
59
+ description: [row.lastAuthorName || row.fromName, row.domainLabel, chatTimeAgo(row.lastMessageTime, settings)].filter(Boolean).join(" · "),
60
+ href: threadHref ? threadHref(row) : mailThreadHref(href, row.id),
61
+ title: row.subject,
62
+ })),
63
+ ...(unread.failed ? [{ value: "retry", label: "Try again", description: unread.status === "ready" ? "The count shown is from before." : undefined }] : []),
64
+ { separator: true },
65
+ {
66
+ value: "inbox",
67
+ label: "Open inbox",
68
+ // What the list leaves out: the threads past the newest eight.
69
+ description: unread.count > shown && shown > 0 ? `${mailUnreadWords(unread.count, maxBadge)} in all` : undefined,
70
+ href,
71
+ },
72
+ ];
73
+ // Said once when the count goes up, by the first bell of the page; an invisible space tells two same words apart.
74
+ const spoken = unread.spoken ? `${unread.spoken.text}${unread.spoken.seq % 2 ? String.fromCharCode(160) : ""}` : "";
75
+ return (_jsxs(_Fragment, { children: [_jsx(Menu, { items: items, align: "end", "aria-label": unread.status === "ready" ? `${label}: ${mailUnreadWords(unread.count, maxBadge).toLowerCase()}` : label, className: "nx-message-bell__menu", onOpenChange: open => open && unread.refresh(), onSelect: value => value === "retry" && unread.refresh(), children: _jsx(Button, { variant: "ghost", size: size, className: cx("nx-message-bell", className), icon: _jsx(InlineIcon, { icon: icon }), badge: badge, "aria-label": words, title: words }) }), unread.speaks && (_jsx("span", { className: "nx-visually-hidden", "aria-live": "polite", children: spoken }))] }));
76
+ }