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,410 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Icon, iconArchive, iconArrowLeft, iconMail, iconMenu2, iconMessageCircle, iconPencil, iconRefresh, iconSearch, iconSend, iconStar, InlineIcon } from "nexus-icons";
4
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { sameChatId } from "../../Helpers/ChatHelpers.js";
6
+ import { cx } from "../../Helpers/ClassHelpers.js";
7
+ import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
8
+ import { formatDateTime } from "../../Helpers/DateTimeHelpers.js";
9
+ import { followMailboxReadMarks, followMailReadMarks, mailDiscussionSource, mailFolderUnread, mailListTime, mailRowKind, mailRowTypeKey, mailTypeLabel, mailTypeOf, mailTypes, readMailSplit, writeMailSplit, } from "../../Helpers/MailClientHelpers.js";
10
+ import { MAIL_LIST_LABELS, mailPeoplePicker, mailPreviewText } from "../../Helpers/MessageHelpers.js";
11
+ import { Button } from "../Documents/Button.js";
12
+ import { SplitButton } from "../Documents/SplitButton.js";
13
+ import { ChatFailure } from "../Chats/ChatParts.js";
14
+ import { ChatPopup } from "../Chats/ChatPopup.js";
15
+ import { TextBox } from "../Inputs/TextBox.js";
16
+ import { Avatar } from "../Viewers/Avatar.js";
17
+ import { MailComposeWindow } from "./MailCompose.js";
18
+ import { MailReader } from "./MailReader.js";
19
+ const FOLDERS = ["inbox", "unread", "sent", "archived"];
20
+ const FOLDER_ICONS = {
21
+ inbox: _jsx(Icon, { name: "inbox" }),
22
+ unread: _jsx(Icon, { name: "mail-exclamation" }),
23
+ sent: _jsx(InlineIcon, { icon: iconSend }),
24
+ archived: _jsx(InlineIcon, { icon: iconArchive }),
25
+ };
26
+ /** The width (px) under which one pane shows at a time, and the one under which the list and the pane stack. */
27
+ const PHONE = 700;
28
+ const messageOf = (error, fallback) => (error instanceof Error && error.message ? error.message : fallback);
29
+ /** A mailbox as a mail client: rail, list and reading pane, with chat popups and compose windows docked at the bottom right. */
30
+ export function MailClient({ source, userId, title = "Message", description, headingLevel = 2, view: viewProp, defaultView, onViewChange, threadId: threadProp, onThreadChange, settings, pageSize = 50, chatSourceFor, renderDomain, onChanged, pollInterval = 60_000, className, }) {
31
+ const [ownView, setOwnView] = useState(defaultView ?? { list: "inbox", type: null });
32
+ const view = viewProp ?? ownView;
33
+ const [ownThread, setOwnThread] = useState(null);
34
+ const openId = threadProp === undefined ? ownThread : threadProp;
35
+ const [rows, setRows] = useState([]);
36
+ const [matches, setMatches] = useState(0);
37
+ const [listState, setListState] = useState({ status: "loading" });
38
+ const [loadingMore, setLoadingMore] = useState(false);
39
+ const [search, setSearch] = useState("");
40
+ const [query, setQuery] = useState("");
41
+ const [selected, setSelected] = useState(() => new Set());
42
+ const [folders, setFolders] = useState(null);
43
+ const [popups, setPopups] = useState([]);
44
+ const [composes, setComposes] = useState([]);
45
+ const [revisions, setRevisions] = useState({});
46
+ const [split, setSplit] = useState("columns");
47
+ const [pane, setPane] = useState("list");
48
+ const [phone, setPhone] = useState(false);
49
+ const [busy, setBusy] = useState(false);
50
+ const [actionError, setActionError] = useState(null);
51
+ const latest = useRef({ onChanged, onViewChange, onThreadChange });
52
+ latest.current = { onChanged, onViewChange, onThreadChange };
53
+ const root = useRef(null);
54
+ const listRef = useRef(null);
55
+ const picker = useMemo(() => mailPeoplePicker(source), [source]);
56
+ // The split the reader chose, read in the browser before it paints; until then the panes are not shown (`data-ready`),
57
+ // so a reader who chose one over the other never sees them jump from side by side.
58
+ const [ready, setReady] = useState(false);
59
+ useLayoutEffect(() => {
60
+ setSplit(readMailSplit(userId));
61
+ setReady(true);
62
+ }, [userId]);
63
+ // How wide the client is: one pane at a time under PHONE.
64
+ useLayoutEffect(() => {
65
+ const element = root.current;
66
+ if (!element)
67
+ return;
68
+ const read = () => {
69
+ if (element.clientWidth > 0)
70
+ setPhone(element.clientWidth < PHONE);
71
+ };
72
+ read();
73
+ const observer = new ResizeObserver(read);
74
+ observer.observe(element);
75
+ return () => observer.disconnect();
76
+ }, []);
77
+ const changeView = useCallback((next) => {
78
+ if (viewProp === undefined)
79
+ setOwnView(next);
80
+ latest.current.onViewChange?.(next);
81
+ setSelected(new Set());
82
+ setPane("list");
83
+ }, [viewProp]);
84
+ const changeThread = useCallback((next) => {
85
+ if (threadProp === undefined)
86
+ setOwnThread(next);
87
+ latest.current.onThreadChange?.(next);
88
+ }, [threadProp]);
89
+ // The search, a moment after the last key.
90
+ useEffect(() => {
91
+ const timer = setTimeout(() => setQuery(search.trim()), 300);
92
+ return () => clearTimeout(timer);
93
+ }, [search]);
94
+ // The list: its first page whenever the folder, the type or the search changes; quietly again on `reload`.
95
+ const [reload, setReload] = useState(0);
96
+ const shownRef = useRef("");
97
+ useEffect(() => {
98
+ const controller = new AbortController();
99
+ const key = `${view.list}|${view.type ?? ""}|${query}`;
100
+ // A new list starts from bones; the same list read again keeps its rows on screen until the answer replaces them.
101
+ if (shownRef.current !== key) {
102
+ shownRef.current = key;
103
+ setRows([]);
104
+ setListState({ status: "loading" });
105
+ }
106
+ const type = mailTypeOf(view.type);
107
+ source
108
+ .inbox({
109
+ list: view.list,
110
+ ...(query && { search: query }),
111
+ ...(type && { threadKind: type.threadKind }),
112
+ ...(type?.domainType !== undefined && { domainType: type.domainType }),
113
+ skip: 0,
114
+ take: pageSize,
115
+ signal: controller.signal,
116
+ })
117
+ .then(page => {
118
+ setRows([...page.rows]);
119
+ setMatches(page.matches ?? page.total ?? page.rows.length);
120
+ setListState({ status: "ready" });
121
+ }, error => {
122
+ if (!controller.signal.aborted)
123
+ setListState({ status: "failed", message: messageOf(error, "The messages could not be loaded.") });
124
+ });
125
+ return () => controller.abort();
126
+ }, [source, view.list, view.type, query, pageSize, reload]);
127
+ const [countsRead, setCountsRead] = useState(0);
128
+ const refreshCounts = useCallback(() => {
129
+ setCountsRead(count => count + 1);
130
+ latest.current.onChanged?.();
131
+ }, []);
132
+ // The read marks the reading pane and the chat windows send, followed: a conversation shows read as soon as it opens,
133
+ // before its mark reaches the server, so the counts wait for the marks on their way and are read again once each is
134
+ // answered (and the page's bell with them, `onChanged`).
135
+ const [marks] = useState(() => ({ pending: new Set(), onSettled: () => undefined }));
136
+ marks.onSettled = refreshCounts;
137
+ const followed = useMemo(() => followMailboxReadMarks(source, marks), [source, marks]);
138
+ // The counts beside the folders and the types, read once no read mark is on its way.
139
+ useEffect(() => {
140
+ const folders = source.folders;
141
+ if (!folders)
142
+ return;
143
+ const controller = new AbortController();
144
+ Promise.all([...marks.pending])
145
+ .then(() => (controller.signal.aborted ? null : folders(controller.signal)))
146
+ .then(next => {
147
+ if (next && !controller.signal.aborted)
148
+ setFolders(next);
149
+ }, () => undefined);
150
+ return () => controller.abort();
151
+ }, [source, countsRead, marks]);
152
+ // Read again quietly while the page is in front: new mail arrives in the list without a reload.
153
+ useEffect(() => {
154
+ if (pollInterval <= 0)
155
+ return;
156
+ const timer = setInterval(() => {
157
+ if (typeof document !== "undefined" && document.visibilityState === "hidden")
158
+ return;
159
+ setReload(count => count + 1);
160
+ setCountsRead(count => count + 1);
161
+ }, pollInterval);
162
+ return () => clearInterval(timer);
163
+ }, [pollInterval]);
164
+ async function loadMore() {
165
+ if (loadingMore || rows.length >= matches)
166
+ return;
167
+ setLoadingMore(true);
168
+ const type = mailTypeOf(view.type);
169
+ try {
170
+ const page = await source.inbox({
171
+ list: view.list,
172
+ ...(query && { search: query }),
173
+ ...(type && { threadKind: type.threadKind }),
174
+ ...(type?.domainType !== undefined && { domainType: type.domainType }),
175
+ skip: rows.length,
176
+ take: pageSize,
177
+ });
178
+ setRows(was => [...was, ...page.rows.filter(row => !was.some(held => sameChatId(held.id, row.id)))]);
179
+ setMatches(page.matches ?? page.total ?? matches);
180
+ }
181
+ catch (error) {
182
+ setActionError(messageOf(error, "More messages could not be loaded."));
183
+ }
184
+ finally {
185
+ setLoadingMore(false);
186
+ }
187
+ }
188
+ // More rows as the end of the list comes into view.
189
+ const sentinel = useRef(null);
190
+ const more = rows.length < matches && listState.status === "ready";
191
+ useEffect(() => {
192
+ const element = sentinel.current;
193
+ if (!element || !more)
194
+ return;
195
+ const observer = new IntersectionObserver(entries => {
196
+ if (entries.some(entry => entry.isIntersecting))
197
+ void loadMore();
198
+ });
199
+ observer.observe(element);
200
+ return () => observer.disconnect();
201
+ });
202
+ const openRow = rows.find(row => openId !== null && sameChatId(row.id, openId)) ?? null;
203
+ // A row follows what the pane read: its unread count. The counts beside the folders are read again when the read mark
204
+ // is answered; a row that was simply out of date (read on another device) has them read now.
205
+ const rowsNow = useRef(rows);
206
+ rowsNow.current = rows;
207
+ const onSummary = useCallback((id, summary) => {
208
+ const held = rowsNow.current.find(row => sameChatId(row.id, id));
209
+ if (!held || held.unreadCount === summary.unreadCount)
210
+ return;
211
+ setRows(was => was.map(row => (sameChatId(row.id, id) ? { ...row, unreadCount: summary.unreadCount } : row)));
212
+ if (marks.pending.size === 0)
213
+ refreshCounts();
214
+ }, [refreshCounts, marks]);
215
+ function openPopup(threadId, kind, head, row) {
216
+ const key = `${kind}:${String(threadId)}`;
217
+ setPopups(was => {
218
+ if (was.some(entry => entry.key === key))
219
+ return [...was.filter(entry => entry.key !== key), was.find(entry => entry.key === key)];
220
+ const thread = followed.threadSource(threadId);
221
+ const subject = head?.subject || row?.subject || "Conversation";
222
+ const entry = kind === "chat"
223
+ ? { key, threadId, title: subject, description: "Chat", source: row && chatSourceFor ? followMailReadMarks(chatSourceFor(row), marks) : thread }
224
+ : { key, threadId, title: `Discussion: ${subject}`, description: "Only the thread's people read it", source: mailDiscussionSource(thread) };
225
+ return [...was, entry];
226
+ });
227
+ }
228
+ // A chat opens in its window as well as in the pane: from a click on its row, and from a link that names it (the page's
229
+ // `threadId`: the bell's, an e-mail copy's), once its kind is known - from its row in the list, or from its head when
230
+ // it is not in the list shown. Once for each opening, so a window closed stays closed while the chat is open.
231
+ const chatOpened = useRef(null);
232
+ function openChatOnce(threadId, head, row) {
233
+ if (chatOpened.current === String(threadId))
234
+ return;
235
+ chatOpened.current = String(threadId);
236
+ openPopup(threadId, "chat", head, row);
237
+ }
238
+ const openRowIsChat = openRow !== null && mailRowKind(openRow) === "chat";
239
+ useEffect(() => {
240
+ if (openId === null || chatOpened.current !== String(openId))
241
+ chatOpened.current = null;
242
+ if (openId !== null && openRow && openRowIsChat)
243
+ openChatOnce(openId, null, openRow);
244
+ // openChatOnce reads the latest rows itself; the row's kind and the conversation are what matter.
245
+ // eslint-disable-next-line react-hooks/exhaustive-deps
246
+ }, [openId, openRowIsChat]);
247
+ function closePopup(key) {
248
+ setPopups(was => was.filter(entry => entry.key !== key));
249
+ }
250
+ function open(row, from) {
251
+ changeThread(row.id);
252
+ setPane("reader");
253
+ // A click brings the chat's window back even when it was closed: it is this opening's window.
254
+ if (mailRowKind(row) === "chat") {
255
+ chatOpened.current = String(row.id);
256
+ openPopup(row.id, "chat", null, row);
257
+ }
258
+ // On a phone, and from the keyboard, the reading starts at the subject.
259
+ if (phone || from === "keyboard")
260
+ requestAnimationFrame(() => root.current?.querySelector(".nx-mailbox__reader [data-nx-focus]")?.focus({ preventScroll: true }));
261
+ }
262
+ function compose(reply) {
263
+ const key = reply ? `reply:${String(reply.threadId)}` : "new";
264
+ setComposes(was => (was.some(entry => entry.key === key) ? [...was.filter(entry => entry.key !== key), was.find(entry => entry.key === key)] : [...was, { key, reply }]));
265
+ }
266
+ function chooseSplit(next) {
267
+ setSplit(next);
268
+ writeMailSplit(userId, next);
269
+ }
270
+ // Mass actions, on the boxes ticked or on the open conversation.
271
+ async function act(action, ids) {
272
+ if (ids.length === 0)
273
+ return;
274
+ setBusy(true);
275
+ setActionError(null);
276
+ try {
277
+ if (action === "archive")
278
+ await source.archive(ids);
279
+ else if (action === "unarchive")
280
+ await source.unarchive(ids);
281
+ else if (action === "markRead")
282
+ await source.markRead?.(ids);
283
+ else
284
+ await source.markUnread?.(ids);
285
+ const keys = new Set(ids.map(String));
286
+ if (action === "archive" || action === "unarchive") {
287
+ // Gone from this folder: Archive takes it out of the Inbox, Move to inbox out of Archived.
288
+ if (view.list !== "sent")
289
+ setRows(was => was.filter(row => !keys.has(String(row.id))));
290
+ if (view.list !== "sent")
291
+ setMatches(count => Math.max(0, count - ids.length));
292
+ if (openId !== null && keys.has(String(openId)) && view.list !== "sent")
293
+ changeThread(null);
294
+ }
295
+ else {
296
+ setRows(was => was.map(row => (keys.has(String(row.id)) ? { ...row, unreadCount: action === "markRead" ? 0 : Math.max(1, row.unreadCount) } : row)));
297
+ if (action === "markUnread" && openId !== null && keys.has(String(openId)))
298
+ changeThread(null);
299
+ }
300
+ setSelected(new Set());
301
+ refreshCounts();
302
+ }
303
+ catch (error) {
304
+ setActionError(messageOf(error, "That could not be done."));
305
+ }
306
+ finally {
307
+ setBusy(false);
308
+ }
309
+ }
310
+ async function toggleStar(row) {
311
+ if (!source.star)
312
+ return;
313
+ const next = !row.starred;
314
+ setRows(was => was.map(one => (sameChatId(one.id, row.id) ? { ...one, starred: next } : one)));
315
+ try {
316
+ await source.star([row.id], next);
317
+ }
318
+ catch (error) {
319
+ setRows(was => was.map(one => (sameChatId(one.id, row.id) ? { ...one, starred: !next } : one)));
320
+ setActionError(messageOf(error, "The star could not be changed."));
321
+ }
322
+ }
323
+ const archivedList = view.list === "archived";
324
+ const massOptions = [
325
+ archivedList ? { value: "unarchive", label: "Move to inbox", icon: _jsx(Icon, { name: "inbox" }) } : { value: "archive", label: "Archive", icon: _jsx(InlineIcon, { icon: iconArchive }) },
326
+ ...(source.markRead ? [{ value: "markRead", label: "Mark read", icon: _jsx(Icon, { name: "mail-opened" }) }] : []),
327
+ ...(source.markUnread ? [{ value: "markUnread", label: "Mark unread", icon: _jsx(InlineIcon, { icon: iconMail }) }] : []),
328
+ ];
329
+ const types = useMemo(() => mailTypes(folders?.kinds ?? []), [folders]);
330
+ const typeEntry = types.find(entry => entry.key === view.type);
331
+ const typeName = view.type ? (typeEntry?.label ?? mailTypeLabel(mailTypeOf(view.type)?.threadKind ?? "mail", mailTypeOf(view.type)?.domainType)) : null;
332
+ const listName = `${MAIL_LIST_LABELS[view.list]}${typeName ? ` · ${typeName}` : ""}`;
333
+ const allTicked = rows.length > 0 && rows.every(row => selected.has(String(row.id)));
334
+ const someTicked = selected.size > 0;
335
+ // The arrow keys move between the rows; Enter or Space opens the one with the focus; Home and End go to the ends.
336
+ function onListKeyDown(event) {
337
+ const buttons = Array.from(listRef.current?.querySelectorAll(".nx-mailbox__open") ?? []);
338
+ const at = buttons.indexOf(document.activeElement);
339
+ let next = -1;
340
+ if (event.key === "ArrowDown")
341
+ next = at < 0 ? 0 : Math.min(buttons.length - 1, at + 1);
342
+ else if (event.key === "ArrowUp")
343
+ next = at < 0 ? 0 : Math.max(0, at - 1);
344
+ else if (event.key === "Home")
345
+ next = 0;
346
+ else if (event.key === "End")
347
+ next = buttons.length - 1;
348
+ else
349
+ return;
350
+ if (next < 0 || !buttons[next])
351
+ return;
352
+ event.preventDefault();
353
+ buttons[next].focus();
354
+ }
355
+ // The row that takes Tab into the list: the open one, else the first.
356
+ const tabRow = openRow ? String(openRow.id) : rows[0] ? String(rows[0].id) : null;
357
+ const preferences = useDatePreferences();
358
+ const zone = useBrowserTimeZone();
359
+ const time = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }) }), [preferences, zone]);
360
+ const [mounted, setMounted] = useState(false);
361
+ // "Today" is the browser's: the server knows no zone, so a row's time is drawn once the page is in the browser.
362
+ useEffect(() => setMounted(true), []);
363
+ const Heading = `h${headingLevel}`;
364
+ const readerActions = openId !== null && (_jsxs(_Fragment, { children: [archivedList || openRow?.archived ? (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(Icon, { name: "inbox" }), "aria-label": "Move to inbox", title: "Move to inbox", disabled: busy, onClick: () => void act("unarchive", [openId]) })) : (_jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconArchive }), "aria-label": "Archive", title: "Archive", disabled: busy, onClick: () => void act("archive", [openId]) })), source.markUnread && _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconMail }), "aria-label": "Mark unread", title: "Mark unread", disabled: busy, onClick: () => void act("markUnread", [openId]) })] }));
365
+ return (_jsxs("div", { ref: root, className: cx("nx-mailbox", className), "data-split": split, "data-pane": pane, "data-ready": ready || undefined, children: [_jsxs("header", { className: "nx-mailbox__bar", children: [title !== false && (_jsxs("div", { className: "nx-mailbox__heading-text", children: [_jsx(Heading, { className: "nx-mailbox__title", children: title }), description && (_jsx("p", { className: "nx-mailbox__lead", title: typeof description === "string" ? description : undefined, children: description }))] })), _jsx(TextBox, { className: "nx-mailbox__search", name: "nx-mailbox-search", "aria-label": "Search mail", placeholder: "Search mail", size: "sm", icon: _jsx(InlineIcon, { icon: iconSearch }), clearable: true, value: search, onValueChange: setSearch }), _jsx(Button, { size: "sm", variant: "ghost", className: "nx-mailbox__split", icon: _jsx(Icon, { name: split === "columns" ? "layout-rows" : "layout-columns" }), "aria-label": split === "columns" ? "Reading pane below the list" : "Reading pane beside the list", title: split === "columns" ? "Reading pane below the list" : "Reading pane beside the list", onClick: () => chooseSplit(split === "columns" ? "rows" : "columns") }), settings] }), _jsxs("div", { className: "nx-mailbox__main", children: [_jsxs("nav", { className: "nx-mailbox__rail", "aria-label": "Folders", children: [_jsx(Button, { variant: "primary", block: true, icon: _jsx(InlineIcon, { icon: iconPencil }), onClick: () => compose(null), children: "Compose" }), _jsx("ul", { className: "nx-mailbox__folders", children: FOLDERS.map(list => {
366
+ const unread = mailFolderUnread(folders, list);
367
+ const current = view.list === list && !view.type;
368
+ return (_jsx("li", { children: _jsxs("button", { type: "button", className: "nx-mailbox__folder", "aria-current": current ? "page" : undefined, "data-active": view.list === list || undefined, onClick: () => changeView({ list, type: null }), children: [_jsx("span", { className: "nx-mailbox__folder-icon", "aria-hidden": "true", children: FOLDER_ICONS[list] }), _jsx("span", { className: "nx-mailbox__folder-name", children: MAIL_LIST_LABELS[list] }), unread > 0 && list !== "sent" && (_jsx("span", { className: "nx-mailbox__count", "aria-label": `${unread} unread`, children: unread }))] }) }, list));
369
+ }) }), types.length > 0 && (_jsxs(_Fragment, { children: [_jsx("h3", { className: "nx-mailbox__heading", children: "Types" }), _jsx("ul", { className: "nx-mailbox__folders", children: types.map(entry => (_jsx("li", { children: _jsxs("button", { type: "button", className: "nx-mailbox__folder", "aria-current": view.type === entry.key ? "page" : undefined, onClick: () => changeView({ list: view.list === "sent" || view.list === "archived" ? "inbox" : view.list, type: view.type === entry.key ? null : entry.key }), title: `${entry.label}: ${entry.total} in the inbox`, children: [_jsx("span", { className: "nx-mailbox__folder-icon", "aria-hidden": "true", children: entry.threadKind === "mail" ? _jsx(InlineIcon, { icon: iconMail }) : entry.threadKind === "chat" ? _jsx(InlineIcon, { icon: iconMessageCircle }) : (entry.icon ?? _jsx(Icon, { name: "clipboard-text" })) }), _jsx("span", { className: "nx-mailbox__folder-name", children: entry.label }), entry.unread > 0 && (_jsx("span", { className: "nx-mailbox__count", "aria-label": `${entry.unread} unread`, children: entry.unread }))] }) }, entry.key))) })] }))] }), _jsxs("section", { className: "nx-mailbox__list", "aria-label": listName, children: [_jsxs("div", { className: "nx-mailbox__tools", children: [_jsx(Button, { size: "sm", variant: "ghost", className: "nx-mailbox__phone-only", icon: _jsx(InlineIcon, { icon: iconMenu2 }), "aria-label": "Folders", title: "Folders", onClick: () => setPane("rail") }), _jsx("label", { className: "nx-choice nx-choice--sm nx-mailbox__check", title: allTicked ? "Select none" : "Select all", children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", "aria-label": allTicked ? "Select none" : "Select all shown", checked: allTicked, ref: element => {
370
+ if (element)
371
+ element.indeterminate = someTicked && !allTicked;
372
+ }, disabled: rows.length === 0, onChange: () => setSelected(allTicked ? new Set() : new Set(rows.map(row => String(row.id)))) }) }), someTicked ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "nx-mailbox__picked", children: [selected.size, " selected"] }), _jsx(SplitButton, { size: "sm", variant: "secondary", options: massOptions, storageKey: "mail-client-mass", loading: busy, onAction: value => void act(value, rows.filter(row => selected.has(String(row.id))).map(row => row.id)) })] })) : (_jsx("span", { className: "nx-mailbox__where", children: listName })), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconRefresh }), "aria-label": "Check for new messages", title: "Check for new messages", onClick: () => {
373
+ setReload(count => count + 1);
374
+ setCountsRead(count => count + 1);
375
+ } })] }), actionError && (_jsx("div", { className: "nx-mailbox__notice", children: _jsx(ChatFailure, { message: actionError }) })), _jsxs("ul", { ref: listRef, className: "nx-mailbox__rows", onKeyDown: onListKeyDown, "aria-busy": listState.status === "loading" || undefined, children: [listState.status === "loading" && rows.length === 0 && Array.from({ length: 8 }, (_, index) => _jsx(MailRowBones, {}, index)), listState.status === "failed" && (_jsx("li", { className: "nx-mailbox__message", children: _jsx(ChatFailure, { message: listState.message, onRetry: () => setReload(count => count + 1) }) })), listState.status === "ready" && rows.length === 0 && (_jsxs("li", { className: "nx-mailbox__message", children: [_jsx(Icon, { name: query ? "mail-off" : "inbox-off" }), _jsx("p", { children: query ? `Nothing in ${listName} matches "${query}".` : `Nothing in ${listName}.` })] })), rows.map(row => (_jsx(MailRow, { row: row, userId: userId, time: time, mounted: mounted, current: openId !== null && sameChatId(row.id, openId), tabbable: tabRow === String(row.id), ticked: selected.has(String(row.id)), stars: !!source.star, onTick: on => setSelected(was => {
376
+ const next = new Set(was);
377
+ if (on)
378
+ next.add(String(row.id));
379
+ else
380
+ next.delete(String(row.id));
381
+ return next;
382
+ }), onStar: () => void toggleStar(row), onOpen: from => open(row, from) }, String(row.id)))), more && (_jsx("li", { ref: sentinel, className: "nx-mailbox__more", children: _jsxs(Button, { size: "sm", variant: "ghost", loading: loadingMore, onClick: () => void loadMore(), children: ["Show more (", rows.length, " of ", matches, ")"] }) }))] })] }), _jsxs("section", { className: "nx-mailbox__reader", "aria-label": "Reading pane", children: [_jsx("div", { className: "nx-mailbox__tools nx-mailbox__phone-only", children: _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconArrowLeft }), onClick: () => setPane("list"), children: listName }) }), openId === null ? (_jsxs("div", { className: "nx-mailbox__message nx-mailbox__message--pane", children: [_jsx(Icon, { name: "mail-opened" }), _jsx("p", { children: "Pick a conversation to read it." })] })) : (_jsx(MailReader, { source: followed, threadId: openId, userId: userId, row: openRow, actions: readerActions, renderDomain: renderDomain, revision: revisions[String(openId)] ?? 0, onHead: head => {
383
+ if (head.threadKind === "chat")
384
+ openChatOnce(openId, head, openRow);
385
+ }, onSummary: summary => onSummary(openId, summary), onDiscuss: (action) => openPopup(action.head.id, mailRowKind(openRow ?? { threadKind: action.head.threadKind, domainType: action.head.domain?.domainType ?? null }) === "chat" ? "chat" : "discussion", action.head, openRow), onReply: (action) => compose({ threadId: action.head.id, subject: action.head.subject, people: replyPeople(action.head, userId) }) }, String(openId)))] })] }), popups.map(entry => (_jsx(ChatPopup, { open: true, onClose: () => closePopup(entry.key), source: entry.source, conversationId: entry.key, userId: userId, title: entry.title, description: entry.description, onSent: () => setRevisions(was => ({ ...was, [String(entry.threadId)]: (was[String(entry.threadId)] ?? 0) + 1 })) }, entry.key))), composes.map(entry => (_jsx(MailComposeWindow, { open: true, onClose: () => setComposes(was => was.filter(one => one.key !== entry.key)), source: source, userId: userId, reply: entry.reply, picker: picker, onSent: result => {
386
+ if (result.reply)
387
+ setRevisions(was => ({ ...was, [String(result.threadId)]: (was[String(result.threadId)] ?? 0) + 1 }));
388
+ setReload(count => count + 1);
389
+ refreshCounts();
390
+ } }, entry.key)))] }));
391
+ }
392
+ /** Everyone a reply in a thread reaches but the reader: its sender and its people, as the reader may see them. */
393
+ function replyPeople(head, userId) {
394
+ const all = [head.from, ...head.to, ...head.cc, ...head.bcc];
395
+ return all.filter((person, index) => !sameChatId(person.id, userId) && all.findIndex(one => sameChatId(one.id, person.id)) === index);
396
+ }
397
+ /** One conversation in the list, as mail: avatar, sender (strong while unread), subject — preview, its type, its time. */
398
+ function MailRow({ row, userId, time, mounted, current, tabbable, ticked, stars, onTick, onStar, onOpen }) {
399
+ const unread = row.unreadCount > 0;
400
+ const kind = mailRowKind(row);
401
+ const sender = sameChatId(row.fromId, userId) && kind !== "chat" ? "You" : row.fromName || "Unknown";
402
+ const preview = mailPreviewText(row.lastPreview);
403
+ const when = mounted ? mailListTime(row.lastMessageTime, time) : "";
404
+ const label = mailTypeLabel(kind, row.domainType);
405
+ return (_jsxs("li", { className: "nx-mailbox__row", "data-unread": unread || undefined, "data-current": current || undefined, children: [_jsx("label", { className: "nx-choice nx-choice--sm nx-mailbox__check", children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: ticked, "aria-label": `Select ${row.subject || sender}`, onChange: event => onTick(event.currentTarget.checked) }) }), stars && (_jsx("button", { type: "button", className: "nx-mailbox__star", "aria-pressed": row.starred === true, "aria-label": row.starred ? "Starred" : "Star", title: row.starred ? "Starred" : "Star", onClick: onStar, children: row.starred ? _jsx(Icon, { name: "star", variant: "filled" }) : _jsx(InlineIcon, { icon: iconStar }) })), _jsxs("button", { type: "button", className: "nx-mailbox__open", tabIndex: tabbable ? 0 : -1, "aria-current": current || undefined, onClick: event => onOpen(event.detail === 0 ? "keyboard" : "pointer"), children: [_jsx(Avatar, { name: row.fromName || sender, src: row.fromAvatar }), _jsxs("span", { className: "nx-mailbox__from", children: [sender, row.messageCount > 1 && kind !== "chat" && _jsx("span", { className: "nx-mailbox__lines", children: row.messageCount })] }), kind === "chat" ? (_jsxs("span", { className: "nx-mailbox__text", children: [_jsx("span", { className: "nx-mailbox__subject", children: row.lastAuthorName ? `${row.lastAuthorName}:` : "" }), _jsxs("span", { className: "nx-mailbox__preview", children: [" ", preview || "No messages yet"] })] })) : (_jsxs("span", { className: "nx-mailbox__text", children: [_jsx("span", { className: "nx-mailbox__subject", children: row.subject || "(no subject)" }), preview && _jsxs("span", { className: "nx-mailbox__preview", children: [" \u2014 ", preview] })] })), _jsx("span", { className: "nx-table__tag nx-table__tag--plain nx-mailbox__chip", "data-kind": kind, title: row.domainLabel ?? label, children: label }), _jsx("time", { className: "nx-mailbox__time", dateTime: row.lastMessageTime, title: row.lastMessageTime ? formatDateTime(row.lastMessageTime, time) : undefined, children: when }), unread && _jsx("span", { className: "nx-visually-hidden", children: ", unread" })] })] }));
406
+ }
407
+ /** A row's bones while the first page loads: the shape of a row, so nothing moves when it lands. */
408
+ function MailRowBones() {
409
+ return (_jsxs("li", { className: "nx-mailbox__row nx-mailbox__row--bones", "aria-hidden": "true", children: [_jsx("span", { className: "nx-mailbox__check" }), _jsxs("span", { className: "nx-mailbox__open", children: [_jsx("span", { className: "nx-avatar nx-mailbox__bone-avatar" }), _jsx("span", { className: "nx-mailbox__from", children: _jsx("span", { className: "nx-table__bone", style: { width: "7rem" } }) }), _jsx("span", { className: "nx-mailbox__text", children: _jsx("span", { className: "nx-table__bone", style: { width: "80%" } }) }), _jsx("span", { className: "nx-mailbox__time", children: _jsx("span", { className: "nx-table__bone", style: { width: "2.5rem" } }) })] })] }));
410
+ }
@@ -0,0 +1,21 @@
1
+ import type { MailInboxRow } from "../../Interfaces/MailInterfaces.ts";
2
+ import type { TableColumn, TableRow } from "../../Interfaces/TableInterfaces.ts";
3
+ export declare const mailInboxColumns: {
4
+ /** Who started the thread. */
5
+ from<TRow extends TableRow = MailInboxRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
6
+ /**
7
+ * The subject, after the record it is about as a tag: first, so a long subject ending in "…" never hides it, and held
8
+ * to part of the column, so a long record's words never hide the subject.
9
+ */
10
+ subject<TRow extends TableRow = MailInboxRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
11
+ /** The last line as text, after the name of who wrote it. */
12
+ lastMessage<TRow extends TableRow = MailInboxRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
13
+ /** When the last line was written, as "14 min ago", the full moment on hover. */
14
+ when<TRow extends TableRow = MailInboxRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
15
+ /** How many lines are unread, in the chat's badge. */
16
+ unread<TRow extends TableRow = MailInboxRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
17
+ };
18
+ /** Every column of a list of threads, in the order an inbox shows them. */
19
+ export declare function mailInboxColumnList<TRow extends TableRow = MailInboxRow>(): TableColumn<TRow>[];
20
+ /** Whether a row of a list of threads reads strong: it holds unread lines. For a table's `rowStrong`. */
21
+ export declare function mailRowUnread(row: TableRow): boolean;
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { chatBadgeText, chatTimeAgo } from "../../Helpers/ChatHelpers.js";
4
+ import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
5
+ import { formatDateTime } from "../../Helpers/DateTimeHelpers.js";
6
+ import { mailPreviewText } from "../../Helpers/MessageHelpers.js";
7
+ import { useEffect, useMemo, useState } from "react";
8
+ const fieldsOf = (row) => row;
9
+ /** How long ago a moment was, in the reader's settings, after the page is in the browser: the server knows no "now" or zone. */
10
+ function MailWhen({ value }) {
11
+ const preferences = useDatePreferences();
12
+ const zone = useBrowserTimeZone();
13
+ const [now, setNow] = useState(null);
14
+ useEffect(() => setNow(Date.now()), [value]);
15
+ const time = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }) }), [preferences, zone]);
16
+ if (!value || now === null)
17
+ return null;
18
+ return (_jsx("time", { dateTime: value, title: formatDateTime(value, time), children: chatTimeAgo(value, time, now) }));
19
+ }
20
+ /** The unread count in the chat's own badge; nothing when all is read. One width either way, so nothing moves as it arrives. */
21
+ function MailUnreadBadge({ count }) {
22
+ if (count <= 0)
23
+ return null;
24
+ return (_jsx("span", { className: "nx-chat-list__badge", title: count === 1 ? "1 unread" : `${count} unread`, children: chatBadgeText(count) }));
25
+ }
26
+ export const mailInboxColumns = {
27
+ /** Who started the thread. */
28
+ from(changes = {}) {
29
+ return { key: "fromName", label: "From", width: "9rem", ...changes };
30
+ },
31
+ /**
32
+ * The subject, after the record it is about as a tag: first, so a long subject ending in "…" never hides it, and held
33
+ * to part of the column, so a long record's words never hide the subject.
34
+ */
35
+ subject(changes = {}) {
36
+ return {
37
+ key: "subject",
38
+ label: "Subject",
39
+ width: "15rem",
40
+ render: row => {
41
+ const { subject, domainLabel } = fieldsOf(row);
42
+ return (_jsxs(_Fragment, { children: [domainLabel && (_jsx("span", { className: "nx-table__tag nx-table__tag--plain nx-mail__label", title: domainLabel, children: domainLabel })), subject] }));
43
+ },
44
+ text: row => [fieldsOf(row).domainLabel, fieldsOf(row).subject].filter(Boolean).join(" "),
45
+ ...changes,
46
+ };
47
+ },
48
+ /** The last line as text, after the name of who wrote it. */
49
+ lastMessage(changes = {}) {
50
+ return {
51
+ key: "lastPreview",
52
+ label: "Last message",
53
+ sortable: false,
54
+ width: "14rem",
55
+ value: row => {
56
+ const { lastAuthorName, lastPreview } = fieldsOf(row);
57
+ const text = mailPreviewText(lastPreview);
58
+ return lastAuthorName ? `${lastAuthorName}: ${text}` : text;
59
+ },
60
+ ...changes,
61
+ };
62
+ },
63
+ /** When the last line was written, as "14 min ago", the full moment on hover. */
64
+ when(changes = {}) {
65
+ return {
66
+ key: "lastMessageTime",
67
+ label: "When",
68
+ width: "7rem",
69
+ render: row => _jsx(MailWhen, { value: fieldsOf(row).lastMessageTime }),
70
+ ...changes,
71
+ };
72
+ },
73
+ /** How many lines are unread, in the chat's badge. */
74
+ unread(changes = {}) {
75
+ return {
76
+ key: "unreadCount",
77
+ label: "Unread",
78
+ width: "4.5rem",
79
+ align: "center",
80
+ render: row => _jsx(MailUnreadBadge, { count: Number(fieldsOf(row).unreadCount) || 0 }),
81
+ ...changes,
82
+ };
83
+ },
84
+ };
85
+ /** Every column of a list of threads, in the order an inbox shows them. */
86
+ export function mailInboxColumnList() {
87
+ return [mailInboxColumns.from(), mailInboxColumns.subject(), mailInboxColumns.lastMessage(), mailInboxColumns.when(), mailInboxColumns.unread()];
88
+ }
89
+ /** Whether a row of a list of threads reads strong: it holds unread lines. For a table's `rowStrong`. */
90
+ export function mailRowUnread(row) {
91
+ return (Number(fieldsOf(row).unreadCount) || 0) > 0;
92
+ }
@@ -0,0 +1,46 @@
1
+ import { type MailComposeDraft } from "../../Helpers/MailClientHelpers.ts";
2
+ import { type PagedPickerParams } from "../../Helpers/PagedPicker.ts";
3
+ import type { ChatId } from "../../Interfaces/ChatInterfaces.ts";
4
+ import type { MailPerson, MessageSource } from "../../Interfaces/MailInterfaces.ts";
5
+ import { type PopupWindowState } from "../Layouts/Popup.tsx";
6
+ /** A reply's thread, as the compose window shows it. */
7
+ export interface MailComposeReply {
8
+ threadId: ChatId;
9
+ subject: string;
10
+ /** Who will read it: the thread's people. */
11
+ people: readonly MailPerson[];
12
+ }
13
+ /** What was sent. */
14
+ export interface MailComposeResult {
15
+ /** The thread: the new one, or the one replied in. */
16
+ threadId: ChatId;
17
+ /** True for a reply. */
18
+ reply: boolean;
19
+ }
20
+ export interface MailComposeWindowProps {
21
+ open: boolean;
22
+ /** Asked to close: after a send, a discard, or a close the reader confirmed. */
23
+ onClose: () => void;
24
+ /** Where the mail goes and the people come from. */
25
+ source: MessageSource;
26
+ /** Who is writing: the draft is theirs alone. */
27
+ userId: ChatId;
28
+ /** A reply in a thread, instead of a new mail. */
29
+ reply?: MailComposeReply | null;
30
+ /** What a new mail opens with when no draft is kept: people by id, a subject, a body. */
31
+ initial?: Partial<Pick<MailComposeDraft, "to" | "cc" | "bcc" | "subject" | "body">>;
32
+ /** To filled from the source's defaults when a new mail opens with no draft and no `initial` people. Default true. */
33
+ useDefaults?: boolean;
34
+ /** The people search, when the page shares one between windows. Default one of the source's own. */
35
+ picker?: PagedPickerParams<MailPerson>;
36
+ /** Where the draft is kept, under the reader. Default `new`, or `reply:<thread>`. */
37
+ draftKey?: string;
38
+ /** After a send. */
39
+ onSent?: (result: MailComposeResult) => void;
40
+ /** The window state, when the page controls it. */
41
+ windowState?: PopupWindowState;
42
+ onWindowStateChange?: (state: PopupWindowState) => void;
43
+ className?: string;
44
+ }
45
+ /** A new mail or a reply, in a docked window. */
46
+ export declare function MailComposeWindow({ open, onClose, source, userId, reply, initial, useDefaults, picker, draftKey, onSent, windowState, onWindowStateChange, className, }: MailComposeWindowProps): import("react").JSX.Element;