nexus-shared 1.1.21 → 3.0.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 +262 -0
- package/LICENSE +21 -0
- package/README.md +1000 -0
- package/dist/Client.Index.d.ts +55 -0
- package/dist/Client.Index.js +60 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +242 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +129 -0
- package/dist/Components/Documents/TabButtons.d.ts +80 -0
- package/dist/Components/Documents/TabButtons.js +38 -0
- package/dist/Components/Forms/ApiForm.d.ts +23 -0
- package/dist/Components/Forms/ApiForm.js +100 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +708 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +323 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +400 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +172 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +37 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +117 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +115 -0
- package/dist/Components/Inputs/RowsInput.js +308 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +27 -0
- package/dist/Components/Inputs/TabularForm.js +348 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +240 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +49 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +596 -0
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +138 -0
- package/dist/Components/Viewers/DataTableParts.js +157 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +96 -0
- package/dist/Helpers/ApiClient.js +200 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +49 -0
- package/dist/Helpers/ApiFormHelpers.js +120 -0
- package/dist/Helpers/ApiModules.d.ts +131 -0
- package/dist/Helpers/ApiModules.js +195 -0
- package/dist/Helpers/ApiResponses.d.ts +51 -0
- package/dist/Helpers/ApiResponses.js +250 -0
- package/dist/Helpers/ApiRoutes.d.ts +140 -0
- package/dist/Helpers/ApiRoutes.js +183 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +98 -0
- package/dist/Helpers/CrudBackend.js +214 -0
- package/dist/Helpers/CrudHelpers.d.ts +111 -0
- package/dist/Helpers/CrudHelpers.js +267 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +233 -0
- package/dist/Helpers/FormStore.js +752 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +252 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +68 -0
- package/dist/Helpers/PopoverHelpers.js +195 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +166 -0
- package/dist/Helpers/RowsStore.js +606 -0
- package/dist/Helpers/TableColumns.d.ts +47 -0
- package/dist/Helpers/TableColumns.js +282 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +110 -0
- package/dist/Helpers/TableHelpers.js +301 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +174 -0
- package/dist/Interfaces/ApiInterfaces.js +30 -0
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/{src/sso-client.tsx → dist/Interfaces/ChatInterfaces.js} +1 -1
- package/dist/Interfaces/CrudInterfaces.d.ts +232 -0
- package/dist/Interfaces/CrudInterfaces.js +1 -0
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +402 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +351 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +95 -0
- package/dist/Services/BrowserApi.js +129 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +54 -0
- package/dist/Services/ServerApi.js +78 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +51 -0
- package/dist/Shared.Index.js +55 -0
- package/package.json +85 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +153 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +161 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +321 -0
- package/src/Styles/Nexus.Index.css +36 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +259 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +596 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +419 -0
- package/src/Styles/Nexus.Table.css +1296 -0
- package/src/Styles/Nexus.Theme.Picker.css +232 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -29
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -133
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -64
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -123
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -53
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -163
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.grid-view.css +0 -42
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { formatDate } from "./DateHelpers.js";
|
|
2
|
+
import { markdownToPlainText } from "./MarkdownHelpers.js";
|
|
3
|
+
import { formatNumber } from "./NumberHelpers.js";
|
|
4
|
+
import { nowUtcDateTime, todayIsoDateIn, toLocalDateTime } from "./DateTimeHelpers.js";
|
|
5
|
+
import { formatTime } from "./TimeHelpers.js";
|
|
6
|
+
/*
|
|
7
|
+
* Pure helpers behind the chat: which conversation a scope names, which of the three looks the trigger icon takes,
|
|
8
|
+
* how messages fall into days and runs by the same author, how a page of messages joins the ones already held, and an
|
|
9
|
+
* in-memory `ChatSource` for demos and tests. No DOM, no React, no network.
|
|
10
|
+
*/
|
|
11
|
+
/** A conversation with no messages yet. */
|
|
12
|
+
export const EMPTY_CHAT_SUMMARY = { messageCount: 0, unreadCount: 0 };
|
|
13
|
+
/** Messages by the same author this close together are drawn as one run, with one avatar and one time. */
|
|
14
|
+
export const CHAT_RUN_MINUTES = 5;
|
|
15
|
+
/** A conversation's key: its domain and its parent id, `"1:482"`. Two scopes of the same record share it, whoever reads. */
|
|
16
|
+
export function chatScopeKey(scope) {
|
|
17
|
+
return `${String(scope.domain)}:${String(scope.parentId)}`;
|
|
18
|
+
}
|
|
19
|
+
/** Whether two scopes name the same conversation for the same reader. */
|
|
20
|
+
export function sameChatScope(a, b) {
|
|
21
|
+
if (!a || !b)
|
|
22
|
+
return a === b;
|
|
23
|
+
return chatScopeKey(a) === chatScopeKey(b) && String(a.userId) === String(b.userId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Which of the three looks the trigger takes: `quiet` with no messages, `active` once the conversation has some, and
|
|
27
|
+
* `unread` once some are this user's to read. The icon never changes size between them.
|
|
28
|
+
*/
|
|
29
|
+
export function chatHighlight(summary) {
|
|
30
|
+
if (!summary)
|
|
31
|
+
return "quiet";
|
|
32
|
+
if (summary.unreadCount > 0)
|
|
33
|
+
return "unread";
|
|
34
|
+
return summary.messageCount > 0 ? "active" : "quiet";
|
|
35
|
+
}
|
|
36
|
+
/** Counts over 99 show as "99+", so a badge never grows wider than its icon. */
|
|
37
|
+
export function chatBadgeText(count, max = 99) {
|
|
38
|
+
return count > max ? `${max}+` : String(count);
|
|
39
|
+
}
|
|
40
|
+
/** Compares ids the way the chat does: id 7 from an API and "7" from a URL are the same person or message. */
|
|
41
|
+
export const sameChatId = (a, b) => a !== null && a !== undefined && b !== null && b !== undefined && String(a) === String(b);
|
|
42
|
+
/** A message's key for React, and for the maps that hold the conversation. */
|
|
43
|
+
export const chatMessageKey = (message) => String(message.clientId ?? message.id);
|
|
44
|
+
/** The time a message is sorted by: its moment, and its id to break a tie. */
|
|
45
|
+
function order(message) {
|
|
46
|
+
return `${message.createdAt}|${String(message.id)}`;
|
|
47
|
+
}
|
|
48
|
+
/** Messages oldest first, as the thread reads. */
|
|
49
|
+
export function sortChatMessages(messages) {
|
|
50
|
+
return [...messages].sort((a, b) => (order(a) < order(b) ? -1 : order(a) > order(b) ? 1 : 0));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Joins a page of messages with the ones already held: oldest first, one entry per message, and a message that came
|
|
54
|
+
* back from the server replacing the one this browser sent (matched by `clientId`, then by id). The newer copy wins,
|
|
55
|
+
* so an edited body and a `sent` status both land.
|
|
56
|
+
*/
|
|
57
|
+
export function mergeChatMessages(held, incoming) {
|
|
58
|
+
if (incoming.length === 0)
|
|
59
|
+
return [...held];
|
|
60
|
+
// A message is known both by its id and by the client id it was sent with, so the copy this browser drew while the
|
|
61
|
+
// message was on its way is replaced in place, not left beside the one the server answered with.
|
|
62
|
+
const keysOf = (message) => (message.clientId ? [`id:${String(message.id)}`, `client:${message.clientId}`] : [`id:${String(message.id)}`]);
|
|
63
|
+
const list = [];
|
|
64
|
+
const places = new Map();
|
|
65
|
+
const place = (message) => {
|
|
66
|
+
const keys = keysOf(message);
|
|
67
|
+
const at = keys.map(key => places.get(key)).find(found => found !== undefined);
|
|
68
|
+
if (at === undefined) {
|
|
69
|
+
list.push(message);
|
|
70
|
+
for (const key of keys)
|
|
71
|
+
places.set(key, list.length - 1);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const merged = { ...list[at], ...message };
|
|
75
|
+
list[at] = merged;
|
|
76
|
+
for (const key of keysOf(merged))
|
|
77
|
+
places.set(key, at);
|
|
78
|
+
};
|
|
79
|
+
for (const message of held)
|
|
80
|
+
place(message);
|
|
81
|
+
for (const message of incoming)
|
|
82
|
+
place(message);
|
|
83
|
+
return sortChatMessages(list);
|
|
84
|
+
}
|
|
85
|
+
/** Messages this user has not read: the ones written by somebody else after `lastReadAt`. */
|
|
86
|
+
export function countChatUnread(messages, userId, lastReadAt) {
|
|
87
|
+
return messages.filter(message => !sameChatId(message.authorId, userId) && !message.isSystem && (!lastReadAt || message.createdAt > lastReadAt)).length;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Where the "New messages" line goes: the id of the first of the last `unreadCount` messages not written by this user.
|
|
91
|
+
* `null` when there is nothing unread. Worked out once, when a conversation opens, so the line does not move while it
|
|
92
|
+
* is being read.
|
|
93
|
+
*/
|
|
94
|
+
export function firstUnreadChatId(messages, userId, unreadCount) {
|
|
95
|
+
if (unreadCount <= 0)
|
|
96
|
+
return null;
|
|
97
|
+
const theirs = messages.filter(message => !sameChatId(message.authorId, userId) && !message.isSystem);
|
|
98
|
+
const first = theirs[Math.max(0, theirs.length - unreadCount)];
|
|
99
|
+
return first ? first.id : null;
|
|
100
|
+
}
|
|
101
|
+
/** The counts of a conversation held in full, for a source that keeps its own messages. */
|
|
102
|
+
export function summarizeChatMessages(messages, userId, lastReadAt) {
|
|
103
|
+
const last = messages[messages.length - 1];
|
|
104
|
+
return {
|
|
105
|
+
messageCount: messages.length,
|
|
106
|
+
unreadCount: countChatUnread(messages, userId, lastReadAt),
|
|
107
|
+
lastMessageAt: last?.createdAt ?? null,
|
|
108
|
+
lastMessageText: last ? chatPreviewText(last.body) : null,
|
|
109
|
+
lastMessageBy: last?.authorName ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** What each state is called, for the mark's tooltip and for a screen reader. */
|
|
113
|
+
export const CHAT_DELIVERY_LABELS = {
|
|
114
|
+
sending: "Sending…",
|
|
115
|
+
failed: "Not sent",
|
|
116
|
+
sent: "Sent",
|
|
117
|
+
delivered: "Delivered",
|
|
118
|
+
read: "Read",
|
|
119
|
+
"read-all": "Read by everyone",
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* How far a message has got. Only the sender's own messages carry a mark, so the reader is left out of both lists: a
|
|
123
|
+
* message is not "read" because the person who wrote it has seen it. Somebody who has left the conversation is left
|
|
124
|
+
* out of "everyone", or a message could never be read by all of it.
|
|
125
|
+
*/
|
|
126
|
+
export function chatDeliveryState(message, userId, participants = []) {
|
|
127
|
+
if (message.status === "sending")
|
|
128
|
+
return "sending";
|
|
129
|
+
if (message.status === "failed")
|
|
130
|
+
return "failed";
|
|
131
|
+
const others = participants.filter(person => !sameChatId(person.id, userId) && !person.inactive);
|
|
132
|
+
const read = (message.readBy ?? []).filter(id => !sameChatId(id, userId));
|
|
133
|
+
if (read.length > 0)
|
|
134
|
+
return others.length > 0 && read.length >= others.length ? "read-all" : "read";
|
|
135
|
+
const delivered = (message.deliveredTo ?? []).filter(id => !sameChatId(id, userId));
|
|
136
|
+
return delivered.length > 0 ? "delivered" : "sent";
|
|
137
|
+
}
|
|
138
|
+
/** A file's size beside its name: "812 B", "248 KB", "3.4 MB". */
|
|
139
|
+
export function chatFileSize(bytes) {
|
|
140
|
+
if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
141
|
+
return "";
|
|
142
|
+
if (bytes < 1024)
|
|
143
|
+
return `${Math.round(bytes)} B`;
|
|
144
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
145
|
+
let size = bytes / 1024;
|
|
146
|
+
let unit = 0;
|
|
147
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
148
|
+
size /= 1024;
|
|
149
|
+
unit++;
|
|
150
|
+
}
|
|
151
|
+
return `${formatNumber(size, { decimals: size < 10 ? 1 : 0 })} ${units[unit]}`;
|
|
152
|
+
}
|
|
153
|
+
/** A message's first line as plain words, cut for a tooltip or a list: a message is Markdown, a preview is not. */
|
|
154
|
+
export function chatPreviewText(body, max = 120) {
|
|
155
|
+
const line = markdownToPlainText(body).replace(/\s+/g, " ").trim();
|
|
156
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : line;
|
|
157
|
+
}
|
|
158
|
+
/** What the day divider says: "Today", "Yesterday", or the date in the user's calendar and format. */
|
|
159
|
+
export function chatDayLabel(isoDate, options = {}, today = todayIsoDateIn(options.timeZone)) {
|
|
160
|
+
if (isoDate === today)
|
|
161
|
+
return "Today";
|
|
162
|
+
if (isoDate === addIsoDays(today, -1))
|
|
163
|
+
return "Yesterday";
|
|
164
|
+
return formatDate(isoDate, options);
|
|
165
|
+
}
|
|
166
|
+
/** A message's time of day, in the user's time format and script: "02:30 PM", "१४:३०". */
|
|
167
|
+
export function chatTimeLabel(utc, options = {}) {
|
|
168
|
+
const local = toLocalDateTime(utc, options.timeZone);
|
|
169
|
+
if (!local)
|
|
170
|
+
return "";
|
|
171
|
+
return formatTime(local.time, { format: options.timeFormat, script: options.script === "devanagari" ? "devanagari" : "latin", seconds: false });
|
|
172
|
+
}
|
|
173
|
+
/** The ISO date a moment falls on in the reader's zone, which is the day its message is grouped under. */
|
|
174
|
+
export function chatLocalDate(utc, timeZone) {
|
|
175
|
+
return toLocalDateTime(utc, timeZone)?.date ?? "";
|
|
176
|
+
}
|
|
177
|
+
/** A day before or after an ISO date, for "Yesterday". */
|
|
178
|
+
function addIsoDays(isoDate, days) {
|
|
179
|
+
const ms = Date.parse(`${isoDate}T00:00:00.000Z`);
|
|
180
|
+
if (!Number.isFinite(ms))
|
|
181
|
+
return isoDate;
|
|
182
|
+
return new Date(ms + days * 86_400_000).toISOString().slice(0, 10);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* How long ago, in few words, for the trigger's tooltip: "just now", "14 min ago", "3 h ago", "yesterday", then the
|
|
186
|
+
* date itself. Never a live-updating clock: the tooltip is read once.
|
|
187
|
+
*/
|
|
188
|
+
export function chatTimeAgo(utc, options = {}, now = Date.now()) {
|
|
189
|
+
if (!utc)
|
|
190
|
+
return "";
|
|
191
|
+
const then = Date.parse(utc);
|
|
192
|
+
if (!Number.isFinite(then))
|
|
193
|
+
return "";
|
|
194
|
+
const minutes = Math.floor((now - then) / 60_000);
|
|
195
|
+
if (minutes < 1)
|
|
196
|
+
return "just now";
|
|
197
|
+
if (minutes < 60)
|
|
198
|
+
return `${minutes} min ago`;
|
|
199
|
+
const hours = Math.floor(minutes / 60);
|
|
200
|
+
if (hours < 24)
|
|
201
|
+
return `${hours} h ago`;
|
|
202
|
+
if (hours < 48)
|
|
203
|
+
return "yesterday";
|
|
204
|
+
return formatDate(chatLocalDate(utc, options.timeZone), options);
|
|
205
|
+
}
|
|
206
|
+
/** What a message with no author name is called. */
|
|
207
|
+
export const CHAT_UNKNOWN_AUTHOR = "Unknown";
|
|
208
|
+
/**
|
|
209
|
+
* Lays a conversation out the way it is drawn: a day at a time, and inside a day, runs of messages by one author sent
|
|
210
|
+
* within `runMinutes` of each other. A system note is always its own run, and the first unread message always starts
|
|
211
|
+
* one, so the "New messages" line never has to split a bubble group.
|
|
212
|
+
*/
|
|
213
|
+
export function groupChatMessages(messages, options) {
|
|
214
|
+
const { userId, unreadFrom = null, runMinutes = CHAT_RUN_MINUTES, today = todayIsoDateIn(options.timeZone) } = options;
|
|
215
|
+
const gap = runMinutes * 60_000;
|
|
216
|
+
const days = [];
|
|
217
|
+
let day = null;
|
|
218
|
+
let run = null;
|
|
219
|
+
for (const message of messages) {
|
|
220
|
+
const date = chatLocalDate(message.createdAt, options.timeZone) || today;
|
|
221
|
+
if (!day || day.date !== date) {
|
|
222
|
+
day = { date, label: chatDayLabel(date, options, today), runs: [] };
|
|
223
|
+
days.push(day);
|
|
224
|
+
run = null;
|
|
225
|
+
}
|
|
226
|
+
const at = Date.parse(message.createdAt);
|
|
227
|
+
const system = message.isSystem === true;
|
|
228
|
+
const unread = unreadFrom !== null && sameChatId(message.id, unreadFrom);
|
|
229
|
+
const joins = run !== null &&
|
|
230
|
+
!unread &&
|
|
231
|
+
run.system === system &&
|
|
232
|
+
sameChatId(run.authorId, message.authorId) &&
|
|
233
|
+
Number.isFinite(at) &&
|
|
234
|
+
Number.isFinite(run.last) &&
|
|
235
|
+
at - run.last <= gap;
|
|
236
|
+
if (joins && run) {
|
|
237
|
+
run.messages.push(message);
|
|
238
|
+
run.last = at;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
run = {
|
|
242
|
+
authorId: message.authorId,
|
|
243
|
+
authorName: message.authorName?.trim() || (system ? "" : CHAT_UNKNOWN_AUTHOR),
|
|
244
|
+
authorAvatar: message.authorAvatar ?? null,
|
|
245
|
+
mine: !system && sameChatId(message.authorId, userId),
|
|
246
|
+
system,
|
|
247
|
+
startsUnread: unread,
|
|
248
|
+
messages: [message],
|
|
249
|
+
last: Number.isFinite(at) ? at : 0,
|
|
250
|
+
};
|
|
251
|
+
day.runs.push(run);
|
|
252
|
+
}
|
|
253
|
+
return days;
|
|
254
|
+
}
|
|
255
|
+
// Conversations at a place
|
|
256
|
+
/** What a conversation is called: a group by its name or its people, a one-to-one by the other person. */
|
|
257
|
+
export function chatConversationName(conversation, userId, fallback = "Conversation") {
|
|
258
|
+
const title = conversation.title?.trim();
|
|
259
|
+
if (title)
|
|
260
|
+
return title;
|
|
261
|
+
const others = (conversation.participants ?? []).filter(person => !sameChatId(person.id, userId));
|
|
262
|
+
if (others.length === 0)
|
|
263
|
+
return conversation.type === "one-to-one" ? fallback : "Just you";
|
|
264
|
+
if (others.length === 1)
|
|
265
|
+
return others[0].name;
|
|
266
|
+
if (others.length === 2)
|
|
267
|
+
return `${others[0].name} and ${others[1].name}`;
|
|
268
|
+
return `${others[0].name}, ${others[1].name} and ${others.length - 2} other${others.length - 2 === 1 ? "" : "s"}`;
|
|
269
|
+
}
|
|
270
|
+
/** The counts of a whole place: every conversation in it added up, which is what one trigger icon stands for. */
|
|
271
|
+
export function chatPlaceSummary(conversations) {
|
|
272
|
+
let messageCount = 0;
|
|
273
|
+
let unreadCount = 0;
|
|
274
|
+
let lastMessageAt = null;
|
|
275
|
+
let lastMessageText = null;
|
|
276
|
+
let lastMessageBy = null;
|
|
277
|
+
for (const conversation of conversations) {
|
|
278
|
+
const summary = conversation.summary;
|
|
279
|
+
if (!summary)
|
|
280
|
+
continue;
|
|
281
|
+
messageCount += summary.messageCount;
|
|
282
|
+
unreadCount += summary.unreadCount;
|
|
283
|
+
if (summary.lastMessageAt && (!lastMessageAt || summary.lastMessageAt > lastMessageAt)) {
|
|
284
|
+
lastMessageAt = summary.lastMessageAt;
|
|
285
|
+
lastMessageText = summary.lastMessageText ?? null;
|
|
286
|
+
lastMessageBy = summary.lastMessageBy ?? null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { messageCount, unreadCount, lastMessageAt, lastMessageText, lastMessageBy, conversationCount: conversations.length };
|
|
290
|
+
}
|
|
291
|
+
/** Which conversation to open: the one with unread messages, else the one with the newest message, else the first. */
|
|
292
|
+
export function pickChatConversation(conversations) {
|
|
293
|
+
if (conversations.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
const unread = conversations.filter(conversation => (conversation.summary?.unreadCount ?? 0) > 0);
|
|
296
|
+
const from = unread.length > 0 ? unread : conversations;
|
|
297
|
+
return [...from].sort((a, b) => (b.summary?.lastMessageAt ?? "").localeCompare(a.summary?.lastMessageAt ?? ""))[0];
|
|
298
|
+
}
|
|
299
|
+
/** A conversation of two is a one-to-one; more, or a name, make a group. */
|
|
300
|
+
export function chatTypeFor(userIds, title) {
|
|
301
|
+
return userIds.length === 1 && !title?.trim() ? "one-to-one" : "group";
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The `@…` the caret sits in, or `null`. An `@` only counts at the start of a word, so an email address does not open
|
|
305
|
+
* the list, and a space ends it — a name of two words is picked from the list rather than typed through.
|
|
306
|
+
*/
|
|
307
|
+
export function findChatMention(value, caret) {
|
|
308
|
+
const before = value.slice(0, caret);
|
|
309
|
+
const at = before.lastIndexOf("@");
|
|
310
|
+
if (at === -1)
|
|
311
|
+
return null;
|
|
312
|
+
// Only at the start of a word: the character before it must be nothing, a space, or a line break.
|
|
313
|
+
const prior = at === 0 ? "" : before[at - 1];
|
|
314
|
+
if (prior && !/[\s(\[{]/.test(prior))
|
|
315
|
+
return null;
|
|
316
|
+
const query = before.slice(at + 1);
|
|
317
|
+
if (/[\s@]/.test(query))
|
|
318
|
+
return null;
|
|
319
|
+
return { at, end: caret, query };
|
|
320
|
+
}
|
|
321
|
+
/** The people an `@…` offers: those in the conversation whose name or role starts with, or contains, what was typed. */
|
|
322
|
+
export function matchChatMentions(participants, query, max = 8) {
|
|
323
|
+
const text = query.trim().toLowerCase();
|
|
324
|
+
const live = participants.filter(person => !person.inactive);
|
|
325
|
+
if (!text)
|
|
326
|
+
return live.slice(0, max);
|
|
327
|
+
// Three tiers, best first: the whole name begins with it, a word of the name does, or it is in there somewhere.
|
|
328
|
+
const begins = [];
|
|
329
|
+
const words = [];
|
|
330
|
+
const holds = [];
|
|
331
|
+
for (const person of live) {
|
|
332
|
+
const name = person.name.toLowerCase();
|
|
333
|
+
if (name.startsWith(text))
|
|
334
|
+
begins.push(person);
|
|
335
|
+
else if (name.split(/\s+/).some(word => word.startsWith(text)))
|
|
336
|
+
words.push(person);
|
|
337
|
+
else if (name.includes(text) || (person.role ?? "").toLowerCase().includes(text))
|
|
338
|
+
holds.push(person);
|
|
339
|
+
}
|
|
340
|
+
return [...begins, ...words, ...holds].slice(0, max);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Puts a name into the text being written, in place of the `@…` that was typed. A message is Markdown, so with
|
|
344
|
+
* `href` the name becomes a link; without one it is plain `@Name`, and the id travels in the draft's `mentions`.
|
|
345
|
+
*/
|
|
346
|
+
export function applyChatMention(value, mention, person, href) {
|
|
347
|
+
const name = person.name.trim();
|
|
348
|
+
const written = href ? `[@${name}](${href}) ` : `@${name} `;
|
|
349
|
+
const next = value.slice(0, mention.at) + written + value.slice(mention.end);
|
|
350
|
+
return { value: next, caret: mention.at + written.length };
|
|
351
|
+
}
|
|
352
|
+
/** The people named with @ in a body, by matching what is written against who is in the conversation. */
|
|
353
|
+
export function readChatMentions(body, participants) {
|
|
354
|
+
const found = [];
|
|
355
|
+
for (const person of participants) {
|
|
356
|
+
const name = person.name.trim();
|
|
357
|
+
if (!name)
|
|
358
|
+
continue;
|
|
359
|
+
// The name as it was put in, plain or as a link; a longer name wins, so "@Sita Rai" is not read as "@Sita".
|
|
360
|
+
if (body.includes(`@${name}`) || body.includes(`[@${name}]`))
|
|
361
|
+
found.push(person.id);
|
|
362
|
+
}
|
|
363
|
+
return found;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A `ChatSource` that keeps everything in memory: the whole chat without a backend, for a demo page, a test, or a
|
|
367
|
+
* page that already has its messages. It answers pages, sends, searches its participants, and keeps the read mark.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* const source = localChatSource({ userId: 7, messages: DEMO_MESSAGES });
|
|
371
|
+
* <ChatPanel scope={scope} source={source} title="Leave request #482" />
|
|
372
|
+
*/
|
|
373
|
+
export function localChatSource(options) {
|
|
374
|
+
let held = sortChatMessages(options.messages ?? []);
|
|
375
|
+
let lastReadAt = options.lastReadAt ?? null;
|
|
376
|
+
let sequence = 0;
|
|
377
|
+
const wait = () => (options.delay ? new Promise(resolve => setTimeout(resolve, options.delay)) : Promise.resolve());
|
|
378
|
+
return {
|
|
379
|
+
messages: () => held,
|
|
380
|
+
async history(request) {
|
|
381
|
+
await wait();
|
|
382
|
+
if (request.after) {
|
|
383
|
+
const from = held.findIndex(message => sameChatId(message.id, request.after));
|
|
384
|
+
return { messages: from === -1 ? [] : held.slice(from + 1), hasMore: false, summary: summarizeChatMessages(held, options.userId, lastReadAt) };
|
|
385
|
+
}
|
|
386
|
+
const end = request.before ? held.findIndex(message => sameChatId(message.id, request.before)) : held.length;
|
|
387
|
+
const stop = end === -1 ? held.length : end;
|
|
388
|
+
const start = Math.max(0, stop - request.take);
|
|
389
|
+
return {
|
|
390
|
+
messages: held.slice(start, stop),
|
|
391
|
+
hasMore: start > 0,
|
|
392
|
+
summary: summarizeChatMessages(held, options.userId, lastReadAt),
|
|
393
|
+
participants: options.participants,
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
async send(draft) {
|
|
397
|
+
await wait();
|
|
398
|
+
if (options.failSend)
|
|
399
|
+
throw new Error("The message could not be sent.");
|
|
400
|
+
const message = {
|
|
401
|
+
id: `local-${++sequence}`,
|
|
402
|
+
authorId: options.userId,
|
|
403
|
+
authorName: options.author?.name ?? "You",
|
|
404
|
+
authorAvatar: options.author?.avatar ?? null,
|
|
405
|
+
body: draft.body,
|
|
406
|
+
createdAt: nowUtcDateTime(),
|
|
407
|
+
status: "sent",
|
|
408
|
+
replyToId: draft.replyToId ?? null,
|
|
409
|
+
...(draft.files?.length && { attachments: draft.files.map(file => ({ name: file.name, size: file.size, contentType: file.type })) }),
|
|
410
|
+
};
|
|
411
|
+
held = [...held, message];
|
|
412
|
+
return message;
|
|
413
|
+
},
|
|
414
|
+
async participants(query) {
|
|
415
|
+
await wait();
|
|
416
|
+
const search = query.search.trim().toLowerCase();
|
|
417
|
+
const all = options.participants ?? [];
|
|
418
|
+
const found = search ? all.filter(person => `${person.name} ${person.role ?? ""}`.toLowerCase().includes(search)) : all;
|
|
419
|
+
return { items: found.slice(query.skip, query.skip + query.pageSize), total: found.length, hasMore: query.skip + query.pageSize < found.length };
|
|
420
|
+
},
|
|
421
|
+
async markRead(upTo) {
|
|
422
|
+
await wait();
|
|
423
|
+
const message = held.find(item => sameChatId(item.id, upTo));
|
|
424
|
+
if (message && (!lastReadAt || message.createdAt > lastReadAt))
|
|
425
|
+
lastReadAt = message.createdAt;
|
|
426
|
+
},
|
|
427
|
+
async remove(id) {
|
|
428
|
+
await wait();
|
|
429
|
+
held = held.filter(message => !sameChatId(message.id, id));
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* A `ChatPlaceSource` that keeps its conversations in memory: a record with several conversations, and starting
|
|
435
|
+
* another, without a backend. A one-to-one with somebody who already has one comes back rather than being made twice,
|
|
436
|
+
* which is the rule the real backend is expected to follow.
|
|
437
|
+
*/
|
|
438
|
+
export function localChatPlace(options) {
|
|
439
|
+
let held = [...(options.conversations ?? [])];
|
|
440
|
+
let sequence = held.length;
|
|
441
|
+
const wait = () => (options.delay ? new Promise(resolve => setTimeout(resolve, options.delay)) : Promise.resolve());
|
|
442
|
+
const everyone = options.participants ?? [];
|
|
443
|
+
return {
|
|
444
|
+
list: () => held,
|
|
445
|
+
async conversations() {
|
|
446
|
+
await wait();
|
|
447
|
+
return [...held].sort((a, b) => (b.summary?.lastMessageAt ?? "").localeCompare(a.summary?.lastMessageAt ?? ""));
|
|
448
|
+
},
|
|
449
|
+
async create(request) {
|
|
450
|
+
await wait();
|
|
451
|
+
if (options.failCreate)
|
|
452
|
+
throw new Error("The conversation could not be started.");
|
|
453
|
+
const type = chatTypeFor(request.userIds, request.title);
|
|
454
|
+
if (type === "one-to-one") {
|
|
455
|
+
const other = request.userIds[0];
|
|
456
|
+
const existing = held.find(conversation => conversation.type === "one-to-one" &&
|
|
457
|
+
(conversation.participants ?? []).some(person => sameChatId(person.id, other)) &&
|
|
458
|
+
(conversation.participants ?? []).some(person => sameChatId(person.id, options.userId)));
|
|
459
|
+
if (existing)
|
|
460
|
+
return existing;
|
|
461
|
+
}
|
|
462
|
+
const people = [
|
|
463
|
+
...everyone.filter(person => request.userIds.some(id => sameChatId(id, person.id))),
|
|
464
|
+
...(everyone.some(person => sameChatId(person.id, options.userId)) ? everyone.filter(person => sameChatId(person.id, options.userId)) : [{ id: options.userId, name: "You" }]),
|
|
465
|
+
];
|
|
466
|
+
const made = {
|
|
467
|
+
id: `local-chat-${++sequence}`,
|
|
468
|
+
type,
|
|
469
|
+
title: request.title?.trim() || null,
|
|
470
|
+
participants: people,
|
|
471
|
+
summary: { ...EMPTY_CHAT_SUMMARY, lastMessageAt: nowUtcDateTime() },
|
|
472
|
+
createdAt: nowUtcDateTime(),
|
|
473
|
+
createdBy: options.userId,
|
|
474
|
+
canSend: true,
|
|
475
|
+
};
|
|
476
|
+
held = [made, ...held];
|
|
477
|
+
return made;
|
|
478
|
+
},
|
|
479
|
+
async participants(query) {
|
|
480
|
+
await wait();
|
|
481
|
+
const search = query.search.trim().toLowerCase();
|
|
482
|
+
const found = (search ? everyone.filter(person => `${person.name} ${person.role ?? ""}`.toLowerCase().includes(search)) : everyone).filter(person => !sameChatId(person.id, options.userId));
|
|
483
|
+
return { items: found.slice(query.skip, query.skip + query.pageSize), total: found.length, hasMore: query.skip + query.pageSize < found.length };
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AnyChatScope, ChatConversation, ChatDomainValue, ChatId, ChatParticipant, ChatPlaceSource, ChatScope, ChatSource, ChatSummary, NewChatRequest } from "../Interfaces/ChatInterfaces.ts";
|
|
2
|
+
import type { ChatSummaryState } from "./ChatSummaries.ts";
|
|
3
|
+
import { ChatStore, type ChatSnapshot } from "./ChatStore.ts";
|
|
4
|
+
export interface UseChatOptions<TDomain extends ChatDomainValue = ChatDomainValue> {
|
|
5
|
+
/** Which conversation: the domain, the record, and the reader. */
|
|
6
|
+
scope: ChatScope<TDomain>;
|
|
7
|
+
/** Which conversation at the place, when the record holds more than one. */
|
|
8
|
+
conversationId?: ChatId;
|
|
9
|
+
/** Where the messages come from. Default: the app's API, through its `ChatBackend`. */
|
|
10
|
+
source?: ChatSource;
|
|
11
|
+
/** Messages per page. Default 30. */
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
/** Milliseconds between checks for new messages. 0 turns it off. Default 15,000. */
|
|
14
|
+
pollInterval?: number;
|
|
15
|
+
/** The counts the page already had, so the header opens filled. */
|
|
16
|
+
summary?: ChatSummary;
|
|
17
|
+
/** The participants the page already had, so the panel makes no call for them. */
|
|
18
|
+
participants?: readonly ChatParticipant[];
|
|
19
|
+
/** Loads and watches only while this is true: a closed panel costs nothing. Default true. */
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** The store and what it holds. The store's methods (`send`, `loadOlder`, `markRead`) are stable; the state is not. */
|
|
23
|
+
export interface ChatHandle {
|
|
24
|
+
store: ChatStore;
|
|
25
|
+
state: ChatSnapshot;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One conversation, kept for as long as the scope stays the same.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const { store, state } = useChat({ scope, enabled: open });
|
|
32
|
+
* state.messages.map(…);
|
|
33
|
+
* store.send(text);
|
|
34
|
+
*/
|
|
35
|
+
export declare function useChat<TDomain extends ChatDomainValue = ChatDomainValue>(options: UseChatOptions<TDomain>): ChatHandle;
|
|
36
|
+
export interface UseChatPlaceOptions<TDomain extends ChatDomainValue = ChatDomainValue> {
|
|
37
|
+
/** Which record: the domain, the record, and the reader. */
|
|
38
|
+
scope: ChatScope<TDomain>;
|
|
39
|
+
/** Where the conversations come from. Default: the app's API, through its `ChatBackend`. */
|
|
40
|
+
place?: ChatPlaceSource;
|
|
41
|
+
/** The conversations the page already had, so the panel makes no call for them. */
|
|
42
|
+
conversations?: readonly ChatConversation[];
|
|
43
|
+
/** Loads only while this is true: a closed panel costs nothing. Default true. */
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** The conversations a record holds, and what can be done with them. */
|
|
47
|
+
export interface ChatPlaceHandle {
|
|
48
|
+
conversations: readonly ChatConversation[];
|
|
49
|
+
loading: boolean;
|
|
50
|
+
/** Why the list could not be read. */
|
|
51
|
+
error: string | null;
|
|
52
|
+
/** A conversation can be started here. */
|
|
53
|
+
canCreate: boolean;
|
|
54
|
+
/** One is being started. */
|
|
55
|
+
creating: boolean;
|
|
56
|
+
/** Reads the list again. */
|
|
57
|
+
reload: () => Promise<void>;
|
|
58
|
+
/** Starts one, and answers with it — the one that already existed, if the backend found one. */
|
|
59
|
+
create: (request: NewChatRequest) => Promise<ChatConversation | null>;
|
|
60
|
+
/** Who can be put in a conversation here. */
|
|
61
|
+
searchParticipants: ChatPlaceSource["participants"];
|
|
62
|
+
/** Writes a conversation's new counts into the list, so the switch and the trigger icon follow a panel. */
|
|
63
|
+
apply: (conversationId: ChatId, summary: ChatSummary) => void;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The conversations one record holds. A leave request can carry several — the manager and the applicant in one, the
|
|
67
|
+
* managers between themselves in another — and this is the list behind the switch, the New chat form, and the one
|
|
68
|
+
* count the trigger icon shows for the record as a whole.
|
|
69
|
+
*/
|
|
70
|
+
export declare function useChatPlace<TDomain extends ChatDomainValue = ChatDomainValue>(options: UseChatPlaceOptions<TDomain>): ChatPlaceHandle;
|
|
71
|
+
/** What a trigger icon knows about a record: its counts, and whether they have arrived. */
|
|
72
|
+
export interface ChatSummaryHandle {
|
|
73
|
+
summary: ChatSummary | undefined;
|
|
74
|
+
state: ChatSummaryState;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The counts one trigger icon draws itself from. Without `given`, it asks the page's cache, which collects every icon
|
|
78
|
+
* that mounted in the same moment and fills them in one call per domain. With `given` — a list whose rows already
|
|
79
|
+
* carry the counts — nothing is asked at all.
|
|
80
|
+
*/
|
|
81
|
+
export declare function useChatSummary(scope: AnyChatScope, given?: ChatSummary): ChatSummaryHandle;
|