stream-chat-react 14.10.0 → 14.11.1
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/README.md +20 -1
- package/dist/cjs/{useNotificationApi.bf76a16b.js → NotificationList.6abbee1c.js} +4935 -1232
- package/dist/cjs/NotificationList.6abbee1c.js.map +1 -0
- package/dist/cjs/{ReactPlayerWrapper.60a93742.js → ReactPlayerWrapper.a94a0cd1.js} +3 -3
- package/dist/cjs/{ReactPlayerWrapper.60a93742.js.map → ReactPlayerWrapper.a94a0cd1.js.map} +1 -1
- package/dist/cjs/channel-detail.js +263 -200
- package/dist/cjs/channel-detail.js.map +1 -1
- package/dist/cjs/emojis.js +31 -15
- package/dist/cjs/emojis.js.map +1 -1
- package/dist/cjs/index.js +2105 -7646
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/useChannelHeaderOnlineStatus.7e17671e.js +961 -0
- package/dist/cjs/useChannelHeaderOnlineStatus.7e17671e.js.map +1 -0
- package/dist/cjs/useInertWhenHidden.22151beb.js +6426 -0
- package/dist/cjs/useInertWhenHidden.22151beb.js.map +1 -0
- package/dist/es/{useNotificationApi.ed6bff88.mjs → NotificationList.db01a56d.mjs} +4556 -1258
- package/dist/es/NotificationList.db01a56d.mjs.map +1 -0
- package/dist/es/channel-detail.mjs +86 -23
- package/dist/es/channel-detail.mjs.map +1 -1
- package/dist/es/emojis.mjs +25 -8
- package/dist/es/emojis.mjs.map +1 -1
- package/dist/es/index.mjs +1045 -6586
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/useChannelHeaderOnlineStatus.a9357343.mjs +771 -0
- package/dist/es/useChannelHeaderOnlineStatus.a9357343.mjs.map +1 -0
- package/dist/es/useInertWhenHidden.3d5fbdba.mjs +5765 -0
- package/dist/es/useInertWhenHidden.3d5fbdba.mjs.map +1 -0
- package/dist/types/components/Attachment/Geolocation.d.ts.map +1 -1
- package/dist/types/components/Attachment/LinkPreview/Card.d.ts.map +1 -1
- package/dist/types/components/Attachment/LinkPreview/CardAudio.d.ts.map +1 -1
- package/dist/types/components/Attachment/ModalGallery.d.ts.map +1 -1
- package/dist/types/components/Attachment/UnsupportedAttachment.d.ts.map +1 -1
- package/dist/types/components/Attachment/VisibilityDisclaimer.d.ts.map +1 -1
- package/dist/types/components/Attachment/components/DownloadButton.d.ts.map +1 -1
- package/dist/types/components/Avatar/Avatar.d.ts +5 -1
- package/dist/types/components/Avatar/Avatar.d.ts.map +1 -1
- package/dist/types/components/Badge/Badge.d.ts.map +1 -1
- package/dist/types/components/Badge/MediaBadge.d.ts.map +1 -1
- package/dist/types/components/BaseImage/ImagePlaceholder.d.ts.map +1 -1
- package/dist/types/components/Button/PlayButton.d.ts.map +1 -1
- package/dist/types/components/Channel/Channel.d.ts.map +1 -1
- package/dist/types/components/ChannelListItem/ChannelListItem.d.ts.map +1 -1
- package/dist/types/components/ChannelListItem/ChannelListItemActionButtons.defaults.d.ts.map +1 -1
- package/dist/types/components/ChannelListItem/ChannelListItemUI.d.ts.map +1 -1
- package/dist/types/components/ChannelListItem/hooks/useMessageDeliveryStatus.d.ts.map +1 -1
- package/dist/types/components/ChatView/ChatView.d.ts.map +1 -1
- package/dist/types/components/Dialog/components/Callout.d.ts.map +1 -1
- package/dist/types/components/Dialog/components/ContextMenu.d.ts +4 -0
- package/dist/types/components/Dialog/components/ContextMenu.d.ts.map +1 -1
- package/dist/types/components/Dialog/components/Prompt.d.ts.map +1 -1
- package/dist/types/components/Dialog/components/Viewer.d.ts.map +1 -1
- package/dist/types/components/EmptyStateIndicator/EmptyStateIndicator.d.ts.map +1 -1
- package/dist/types/components/Form/NumericInput.d.ts.map +1 -1
- package/dist/types/components/Form/TextInput.d.ts.map +1 -1
- package/dist/types/components/Gallery/GalleryHeader.d.ts.map +1 -1
- package/dist/types/components/Gallery/GalleryUI.d.ts.map +1 -1
- package/dist/types/components/Icons/index.d.ts +1 -0
- package/dist/types/components/Icons/index.d.ts.map +1 -1
- package/dist/types/components/Icons/slots.d.ts +10 -0
- package/dist/types/components/Icons/slots.d.ts.map +1 -0
- package/dist/types/components/Loading/LoadingIndicator.d.ts +2 -2
- package/dist/types/components/Loading/LoadingIndicator.d.ts.map +1 -1
- package/dist/types/components/Location/ShareLocationDialog.d.ts.map +1 -1
- package/dist/types/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.d.ts.map +1 -1
- package/dist/types/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.d.ts.map +1 -1
- package/dist/types/components/MediaRecorder/AudioRecorder/AudioRecordingPlayback.d.ts.map +1 -1
- package/dist/types/components/MediaRecorder/AudioRecorder/AudioRecordingPreview.d.ts.map +1 -1
- package/dist/types/components/Message/MessageAlsoSentInChannelIndicator.d.ts.map +1 -1
- package/dist/types/components/Message/MessageDeletedBubble.d.ts.map +1 -1
- package/dist/types/components/Message/MessageStatus.d.ts.map +1 -1
- package/dist/types/components/Message/MessageTranslationIndicator.d.ts.map +1 -1
- package/dist/types/components/Message/PinIndicator.d.ts.map +1 -1
- package/dist/types/components/Message/ReminderNotification.d.ts.map +1 -1
- package/dist/types/components/MessageActions/DownloadSubmenu.d.ts.map +1 -1
- package/dist/types/components/MessageActions/MessageActions.defaults.d.ts.map +1 -1
- package/dist/types/components/MessageActions/RemindMeSubmenu.d.ts.map +1 -1
- package/dist/types/components/MessageBounce/MessageBouncePrompt.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentPreviewList/AudioAttachmentPreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentPreviewList/FileAttachmentPreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentPreviewList/GeolocationPreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentPreviewList/MediaAttachmentPreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentPreviewList/UnsupportedAttachmentPreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentSelector/AttachmentSelector.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/AttachmentSelector/CommandsMenu.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/CommandChip.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/LinkPreviewList.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/MessageComposer.d.ts +10 -1
- package/dist/types/components/MessageComposer/MessageComposer.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/MessageComposerActions.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/QuotedMessagePreview.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/RemoveAttachmentPreviewButton.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/SendButton.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/SendToChannelCheckbox.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/WithDragAndDropUpload.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/hooks/useMessageComposerCommands.d.ts.map +1 -1
- package/dist/types/components/MessageComposer/hooks/useMessageComposerController.d.ts.map +1 -1
- package/dist/types/components/MessageList/ScrollToLatestMessageButton.d.ts.map +1 -1
- package/dist/types/components/MessageList/UnreadMessagesNotification.d.ts.map +1 -1
- package/dist/types/components/MessageList/UnreadMessagesSeparator.d.ts.map +1 -1
- package/dist/types/components/MessageList/hooks/useMarkRead.d.ts.map +1 -1
- package/dist/types/components/Modal/CloseButtonOnModalOverlay.d.ts.map +1 -1
- package/dist/types/components/Notifications/Notification.d.ts.map +1 -1
- package/dist/types/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.d.ts.map +1 -1
- package/dist/types/components/Poll/PollCreationDialog/OptionFieldSet.d.ts.map +1 -1
- package/dist/types/components/Poll/PollCreationDialog/PollCreationDialogControls.d.ts.map +1 -1
- package/dist/types/components/Poll/PollCreationDialog/PollOptionReorderHandle.d.ts.map +1 -1
- package/dist/types/components/Reactions/MessageReactionsDetail.d.ts.map +1 -1
- package/dist/types/components/Reactions/ReactionSelector.d.ts.map +1 -1
- package/dist/types/components/Reactions/ReactionSelectorWithButton.d.ts +6 -1
- package/dist/types/components/Reactions/ReactionSelectorWithButton.d.ts.map +1 -1
- package/dist/types/components/SummarizedMessagePreview/SummarizedMessagePreview.d.ts.map +1 -1
- package/dist/types/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.d.ts.map +1 -1
- package/dist/types/components/TextareaComposer/SuggestionList/MentionItem/RoleItem.d.ts.map +1 -1
- package/dist/types/components/TextareaComposer/SuggestionList/MentionItem/UserGroupItem.d.ts.map +1 -1
- package/dist/types/components/Thread/ThreadHeader.d.ts.map +1 -1
- package/dist/types/components/Threads/ThreadList/ThreadListEmptyPlaceholder.d.ts.map +1 -1
- package/dist/types/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.d.ts.map +1 -1
- package/dist/types/components/VideoPlayer/VideoThumbnail.d.ts.map +1 -1
- package/dist/types/context/ComponentContext.d.ts +3 -0
- package/dist/types/context/ComponentContext.d.ts.map +1 -1
- package/dist/types/context/WithComponents.d.ts.map +1 -1
- package/dist/types/context/index.d.ts +1 -0
- package/dist/types/context/index.d.ts.map +1 -1
- package/dist/types/context/useComponentContextIcons.d.ts +16 -0
- package/dist/types/context/useComponentContextIcons.d.ts.map +1 -0
- package/dist/types/plugins/ChannelDetail/ChannelDetail.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/ChannelDetailEmptyList.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/ChannelDetailSearchInput.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.d.ts.map +1 -1
- package/dist/types/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.d.ts.map +1 -1
- package/dist/types/plugins/Emojis/EmojiPicker.d.ts +6 -1
- package/dist/types/plugins/Emojis/EmojiPicker.d.ts.map +1 -1
- package/dist/types/plugins/Emojis/Picker.d.ts +7 -0
- package/dist/types/plugins/Emojis/Picker.d.ts.map +1 -0
- package/dist/types/utils/getChannelConfig.d.ts +8 -0
- package/dist/types/utils/getChannelConfig.d.ts.map +1 -0
- package/package.json +3 -9
- package/AI.md +0 -423
- package/dist/cjs/useChannelHeaderOnlineStatus.6ace991c.js +0 -4485
- package/dist/cjs/useChannelHeaderOnlineStatus.6ace991c.js.map +0 -1
- package/dist/cjs/useMessageComposerController.f8f20e3d.js +0 -99
- package/dist/cjs/useMessageComposerController.f8f20e3d.js.map +0 -1
- package/dist/cjs/useNotificationApi.bf76a16b.js.map +0 -1
- package/dist/es/useChannelHeaderOnlineStatus.d6065c22.mjs +0 -3899
- package/dist/es/useChannelHeaderOnlineStatus.d6065c22.mjs.map +0 -1
- package/dist/es/useMessageComposerController.d50f6168.mjs +0 -69
- package/dist/es/useMessageComposerController.d50f6168.mjs.map +0 -1
- package/dist/es/useNotificationApi.ed6bff88.mjs.map +0 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { Lt as useAriaIdentifiers, Nt as Avatar, Qn as useModalContext, Sr as useComponentContextIcons, Vn as useTranslationContext, Xt as isMessageDeleted, ca as useChatContext, da as useChannelStateContext, mt as extractDisplayInfo } from "./NotificationList.db01a56d.mjs";
|
|
2
|
+
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { useSyncExternalStore } from "use-sync-external-store/shim";
|
|
6
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
7
|
+
import ReactMarkdown from "react-markdown";
|
|
8
|
+
import remarkGfm from "remark-gfm";
|
|
9
|
+
import { toString } from "mdast-util-to-string";
|
|
10
|
+
import remarkParse from "remark-parse";
|
|
11
|
+
import { unified } from "unified";
|
|
12
|
+
//#region src/context/MessageTranslationViewContext.tsx
|
|
13
|
+
/**
|
|
14
|
+
* Message translation view context: user-specific state for whether each message
|
|
15
|
+
* shows original text or a translation.
|
|
16
|
+
*
|
|
17
|
+
* ## Spec
|
|
18
|
+
*
|
|
19
|
+
* - **State**: Per message list (channel vs thread), we store a map
|
|
20
|
+
* `messageId → 'original' | 'translated'`. Default for messages with `message.i18n`
|
|
21
|
+
* is `'translated'`; otherwise `'original'`.
|
|
22
|
+
*
|
|
23
|
+
* - **Provider placement**: The provider is tied to the **message list**, not the channel.
|
|
24
|
+
* It is rendered inside `MessageList` and `VirtualizedMessageList`. That way the
|
|
25
|
+
* main channel list and the thread list each have their own translation view state
|
|
26
|
+
* (e.g. Thread.tsx gets correct behavior without sharing channel state).
|
|
27
|
+
*
|
|
28
|
+
* - **Multiple translations**: `message.i18n` can contain multiple languages, e.g.:
|
|
29
|
+
* `{ en_text: "Good morning", fr_text: "Bonjour", it_text: "Buongiorno", language: "en" }`.
|
|
30
|
+
* Which translation is shown is determined by the app’s **user language**
|
|
31
|
+
* (`useTranslationContext().userLanguage`). We use `message.i18n[userLanguage + '_text']`;
|
|
32
|
+
* if missing, we fall back to `message.text`. Only one translation is shown at a time.
|
|
33
|
+
*
|
|
34
|
+
* - **Source language**: When present, `message.i18n.language` is the original/source
|
|
35
|
+
* language of `message.text`. It can be used for the indicator label, e.g.
|
|
36
|
+
* "Translated from English · View original".
|
|
37
|
+
*
|
|
38
|
+
* - **Invariants**: The original message content, layout, grouping, and order stay
|
|
39
|
+
* unchanged. Removing or toggling translation only changes the annotation and which
|
|
40
|
+
* text is displayed. "View original" / "View translation" switch the displayed
|
|
41
|
+
* text and update the annotation (e.g. "Original · View translation" when showing
|
|
42
|
+
* original).
|
|
43
|
+
*
|
|
44
|
+
* - **Translation indicator visibility**: The translation indicator (e.g. "Translated ·
|
|
45
|
+
* View original") is **not** shown when the currently viewed text already corresponds
|
|
46
|
+
* to `userLanguage` — for example when viewing original and the original text is the
|
|
47
|
+
* user-language version (i.e. `getTranslatedMessageText({ language: userLanguage, message })`
|
|
48
|
+
* equals `message.text`). In that case there is no meaningful original/translated choice,
|
|
49
|
+
* so the indicator is hidden.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Returns the translated message text for a given language from `message.i18n`, or
|
|
53
|
+
* undefined if not present. Used to resolve which of the multiple translations to show.
|
|
54
|
+
*/
|
|
55
|
+
var getTranslatedMessageText = ({ language, message }) => message?.i18n?.[`${language}_text`];
|
|
56
|
+
var defaultContextValue = {
|
|
57
|
+
getTranslationView: (_messageId, hasI18n) => hasI18n ? "translated" : "original",
|
|
58
|
+
setTranslationView: () => {}
|
|
59
|
+
};
|
|
60
|
+
var MessageTranslationViewContext = createContext(defaultContextValue);
|
|
61
|
+
var MessageTranslationViewProvider = ({ children }) => {
|
|
62
|
+
const [viewByMessageId, setViewByMessageId] = useState({});
|
|
63
|
+
const setTranslationView = useCallback((messageId, view) => {
|
|
64
|
+
setViewByMessageId((prev) => ({
|
|
65
|
+
...prev,
|
|
66
|
+
[messageId]: view
|
|
67
|
+
}));
|
|
68
|
+
}, []);
|
|
69
|
+
const getTranslationView = useCallback((messageId, hasI18n) => viewByMessageId[messageId] ?? (hasI18n ? "translated" : "original"), [viewByMessageId]);
|
|
70
|
+
const stableValue = React.useMemo(() => ({
|
|
71
|
+
getTranslationView,
|
|
72
|
+
setTranslationView
|
|
73
|
+
}), [getTranslationView, setTranslationView]);
|
|
74
|
+
return /* @__PURE__ */ jsx(MessageTranslationViewContext.Provider, {
|
|
75
|
+
value: stableValue,
|
|
76
|
+
children
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
var useMessageTranslationViewContext = () => {
|
|
80
|
+
return useContext(MessageTranslationViewContext) ?? defaultContextValue;
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/utils/useStableCallback.ts
|
|
84
|
+
/**
|
|
85
|
+
* A utility hook implementing a stable callback. It takes in an unstable method that
|
|
86
|
+
* is supposed to be invoked somewhere deeper in the DOM tree without making it
|
|
87
|
+
* change its reference every time the parent component rerenders. It will also return
|
|
88
|
+
* the value of the callback if it does return one.
|
|
89
|
+
* A common use-case would be having a function whose invocation depends on state
|
|
90
|
+
* somewhere high up in the DOM tree and wanting to use the same function deeper
|
|
91
|
+
* down, for example in a leaf node and simply using useCallback results in
|
|
92
|
+
* cascading dependency hell. If we wrap it in useStableCallback, we would be able
|
|
93
|
+
* to:
|
|
94
|
+
* - Use the same function as a dependency of another hook (since it is stable)
|
|
95
|
+
* - Still invoke it and get the latest state
|
|
96
|
+
*
|
|
97
|
+
* **Caveats:**
|
|
98
|
+
* - Never wrap a function that is supposed to return a React.ReactElement in
|
|
99
|
+
* useStableCallback, since React will not know that the DOM needs to be updated
|
|
100
|
+
* whenever the callback value changes (for example, renderItem from FlatList must
|
|
101
|
+
* never be wrapped in this hook)
|
|
102
|
+
* - Always prefer using a standard useCallback/stable function wherever possible
|
|
103
|
+
* (the purpose of useStableCallback is to bridge the gap between top level contexts
|
|
104
|
+
* and cascading rereders in downstream components - **not** as an escape hatch)
|
|
105
|
+
* @param callback - the callback we want to stabilize
|
|
106
|
+
*/
|
|
107
|
+
var useStableCallback = (callback) => {
|
|
108
|
+
const ref = useRef(callback);
|
|
109
|
+
ref.current = callback;
|
|
110
|
+
return useCallback((...args) => ref.current(...args), []);
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/components/Dialog/components/Alert.tsx
|
|
114
|
+
var Root = forwardRef(function AlertRoot({ children, className, ...props }, ref) {
|
|
115
|
+
return /* @__PURE__ */ jsx("div", {
|
|
116
|
+
...props,
|
|
117
|
+
className: clsx("str-chat__alert-root", className),
|
|
118
|
+
ref,
|
|
119
|
+
children
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
var Header = forwardRef(function AlertHeader({ children, className, description, descriptionId, Icon, title, titleId, ...props }, ref) {
|
|
123
|
+
const { dialogId } = useModalContext();
|
|
124
|
+
const { descriptionId: derivedDescriptionId, titleId: derivedTitleId } = useAriaIdentifiers(dialogId);
|
|
125
|
+
const resolvedTitleId = titleId ?? derivedTitleId;
|
|
126
|
+
const resolvedDescriptionId = descriptionId ?? derivedDescriptionId;
|
|
127
|
+
return /* @__PURE__ */ jsx("div", {
|
|
128
|
+
...props,
|
|
129
|
+
className: clsx("str-chat__alert-header", className),
|
|
130
|
+
ref,
|
|
131
|
+
children: title ? /* @__PURE__ */ jsxs(Fragment$1, { children: [Icon && /* @__PURE__ */ jsx(Icon, {}), /* @__PURE__ */ jsxs("div", {
|
|
132
|
+
className: "str-chat__alert-header__copy",
|
|
133
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
134
|
+
className: "str-chat__alert-header__title",
|
|
135
|
+
id: resolvedTitleId,
|
|
136
|
+
children: title
|
|
137
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
138
|
+
className: "str-chat__alert-header__description",
|
|
139
|
+
id: resolvedDescriptionId,
|
|
140
|
+
children: description
|
|
141
|
+
})]
|
|
142
|
+
})] }) : children
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
var Alert = {
|
|
146
|
+
Actions: forwardRef(function AlertActions({ children, className, ...props }, ref) {
|
|
147
|
+
return /* @__PURE__ */ jsx("div", {
|
|
148
|
+
...props,
|
|
149
|
+
className: clsx("str-chat__alert-actions", className),
|
|
150
|
+
ref,
|
|
151
|
+
children
|
|
152
|
+
});
|
|
153
|
+
}),
|
|
154
|
+
Header,
|
|
155
|
+
Root
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/components/Badge/Badge.tsx
|
|
159
|
+
/**
|
|
160
|
+
* Compact pill/circle badge for counts and labels.
|
|
161
|
+
* Uses design tokens: --badge-bg-*, --badge-text-*, --badge-border.
|
|
162
|
+
*/
|
|
163
|
+
var Badge = ({ children, className, size = "md", variant = "default", ...spanProps }) => /* @__PURE__ */ jsx("div", {
|
|
164
|
+
...spanProps,
|
|
165
|
+
className: clsx("str-chat__badge", `str-chat__badge--variant-${variant}`, { [`str-chat__badge--size-${size}`]: size }, className),
|
|
166
|
+
children
|
|
167
|
+
});
|
|
168
|
+
var ErrorBadge = ({ className, size = "sm", ...rest }) => {
|
|
169
|
+
const { IconExclamationMarkFill } = useComponentContextIcons();
|
|
170
|
+
return /* @__PURE__ */ jsx(Badge, {
|
|
171
|
+
...rest,
|
|
172
|
+
className,
|
|
173
|
+
size,
|
|
174
|
+
variant: "error",
|
|
175
|
+
children: /* @__PURE__ */ jsx(IconExclamationMarkFill, {})
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/components/Avatar/ChannelAvatar.tsx
|
|
180
|
+
var ChannelAvatar = ({ displayMembers, imageUrl, size, userName, ...sharedProps }) => {
|
|
181
|
+
return /* @__PURE__ */ jsx(GroupAvatar, {
|
|
182
|
+
displayMembers: useMemo(() => {
|
|
183
|
+
if (!imageUrl && displayMembers && displayMembers.length > 0) return displayMembers;
|
|
184
|
+
return [{
|
|
185
|
+
imageUrl,
|
|
186
|
+
userName
|
|
187
|
+
}];
|
|
188
|
+
}, [
|
|
189
|
+
displayMembers,
|
|
190
|
+
imageUrl,
|
|
191
|
+
userName
|
|
192
|
+
]),
|
|
193
|
+
size,
|
|
194
|
+
...sharedProps
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/components/Avatar/GroupAvatar.tsx
|
|
199
|
+
/**
|
|
200
|
+
* Avatar component to display multiple users' avatars in a group.
|
|
201
|
+
* Renders a single Avatar if fewer than 2 members. Otherwise, renders up to 2 avatars (when overflowCount is set) or 4, plus an optional +N badge.
|
|
202
|
+
*/
|
|
203
|
+
var GroupAvatar = ({ badgeSize, className, displayMembers = [], isOnline, size, ...rest }) => {
|
|
204
|
+
const displayMembersToRender = useMemo(() => displayMembers.length > 4 ? displayMembers.slice(0, 2) : displayMembers, [displayMembers]);
|
|
205
|
+
const overflowCount = displayMembers.length - displayMembersToRender.length;
|
|
206
|
+
if (displayMembers.length < 2) {
|
|
207
|
+
const firstUser = displayMembers[0];
|
|
208
|
+
return /* @__PURE__ */ jsx(Avatar, {
|
|
209
|
+
imageUrl: firstUser?.imageUrl,
|
|
210
|
+
isOnline,
|
|
211
|
+
size,
|
|
212
|
+
userName: firstUser?.userName,
|
|
213
|
+
...rest
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
let avatarSize = null;
|
|
217
|
+
if (size === "2xl") avatarSize = "lg";
|
|
218
|
+
else if (size === "xl") avatarSize = "md";
|
|
219
|
+
else if (size === "lg") avatarSize = "sm";
|
|
220
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
221
|
+
className: clsx("str-chat__avatar-group", {
|
|
222
|
+
"str-chat__avatar-group--offline": typeof isOnline === "boolean" && !isOnline,
|
|
223
|
+
"str-chat__avatar-group--online": typeof isOnline === "boolean" && isOnline,
|
|
224
|
+
[`str-chat__avatar-group--size-${size}`]: typeof size === "string"
|
|
225
|
+
}, className),
|
|
226
|
+
"data-testid": "group-avatar",
|
|
227
|
+
role: "button",
|
|
228
|
+
...rest,
|
|
229
|
+
children: [displayMembersToRender.map(({ id, imageUrl, userName }, index) => /* @__PURE__ */ jsx(Avatar, {
|
|
230
|
+
imageUrl,
|
|
231
|
+
size: avatarSize,
|
|
232
|
+
userName
|
|
233
|
+
}, id || `${userName}-${imageUrl}-${index}`)), typeof overflowCount === "number" && overflowCount > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
234
|
+
className: "str-chat__avatar-group__count-badge",
|
|
235
|
+
"data-testid": "group-avatar-count-badge",
|
|
236
|
+
size: badgeSize,
|
|
237
|
+
variant: "counter",
|
|
238
|
+
children: ["+", overflowCount]
|
|
239
|
+
})]
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/components/Message/renderText/remarkPlugins/htmlToTextPlugin.ts
|
|
244
|
+
var visitor = (node) => {
|
|
245
|
+
if (node.type !== "html") return;
|
|
246
|
+
node.type = "text";
|
|
247
|
+
};
|
|
248
|
+
var transform = (tree) => {
|
|
249
|
+
visit(tree, visitor);
|
|
250
|
+
};
|
|
251
|
+
var htmlToTextPlugin = () => transform;
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/components/Message/renderText/remarkPlugins/imageToLink.ts
|
|
254
|
+
var text = (value) => ({
|
|
255
|
+
type: "text",
|
|
256
|
+
value
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* Converts image Markdown links ()
|
|
260
|
+
* to HTML <a href={url}>{url | title | alt}</a>
|
|
261
|
+
*
|
|
262
|
+
* By default, the anchor text content is the image url so that image preview can be generated / enriched on the server.
|
|
263
|
+
* @param getTextLabelFrom
|
|
264
|
+
*/
|
|
265
|
+
function imageToLink({ getTextLabelFrom = "url" } = {}) {
|
|
266
|
+
return (tree) => {
|
|
267
|
+
const visitor = (node, index, parent) => {
|
|
268
|
+
if (parent == null || index == null) return;
|
|
269
|
+
const link = {
|
|
270
|
+
children: [text(node[getTextLabelFrom] ?? node.url)],
|
|
271
|
+
title: node.title ?? node.alt ?? node.url,
|
|
272
|
+
type: "link",
|
|
273
|
+
url: node.url
|
|
274
|
+
};
|
|
275
|
+
parent.children.splice(index, 1, link);
|
|
276
|
+
return [SKIP, index + 1];
|
|
277
|
+
};
|
|
278
|
+
visit(tree, "image", visitor);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/components/Message/renderText/remarkPlugins/plusPlusToEmphasis.ts
|
|
283
|
+
/**
|
|
284
|
+
* \S → first char must be non-whitespace
|
|
285
|
+
* (?:...)?→ optional middle+closing when length > 1
|
|
286
|
+
* [\s\S]*?→ anything (including newlines), lazy
|
|
287
|
+
* final \S→ last char non-whitespace (only required when there’s more than 1)
|
|
288
|
+
*
|
|
289
|
+
* Matches:
|
|
290
|
+
* ++a++
|
|
291
|
+
* Does not match:
|
|
292
|
+
* ++++
|
|
293
|
+
* ++ ++
|
|
294
|
+
*/
|
|
295
|
+
var INS_REGEX = /\+\+(\S(?:[\s\S]*?\S)?)\+\+/g;
|
|
296
|
+
var IGNORE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
297
|
+
"code",
|
|
298
|
+
"inlineCode",
|
|
299
|
+
"link",
|
|
300
|
+
"linkReference",
|
|
301
|
+
"definition",
|
|
302
|
+
"math",
|
|
303
|
+
"inlineMath"
|
|
304
|
+
]);
|
|
305
|
+
/**
|
|
306
|
+
* Converts MD "++Some text++" to inserted text element rendered in HTML as <ins>Some text</ins>
|
|
307
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins
|
|
308
|
+
*/
|
|
309
|
+
var plusPlusToEmphasis = () => {
|
|
310
|
+
const visitor = (node, index, parent) => {
|
|
311
|
+
if (IGNORE_NODE_TYPES.has(node.type)) return SKIP;
|
|
312
|
+
if (node.type !== "text" || parent == null || typeof index !== "number") return;
|
|
313
|
+
const value = node.value;
|
|
314
|
+
INS_REGEX.lastIndex = 0;
|
|
315
|
+
let match;
|
|
316
|
+
let last = 0;
|
|
317
|
+
const out = [];
|
|
318
|
+
while (match = INS_REGEX.exec(value)) {
|
|
319
|
+
const [full, inner] = match;
|
|
320
|
+
const start = match.index;
|
|
321
|
+
if (start > last) out.push({
|
|
322
|
+
type: "text",
|
|
323
|
+
value: value.slice(last, start)
|
|
324
|
+
});
|
|
325
|
+
out.push({
|
|
326
|
+
children: [{
|
|
327
|
+
type: "text",
|
|
328
|
+
value: inner
|
|
329
|
+
}],
|
|
330
|
+
data: { hName: "ins" },
|
|
331
|
+
type: "emphasis"
|
|
332
|
+
});
|
|
333
|
+
last = start + full.length;
|
|
334
|
+
}
|
|
335
|
+
if (out.length === 0) return;
|
|
336
|
+
if (last < value.length) out.push({
|
|
337
|
+
type: "text",
|
|
338
|
+
value: value.slice(last)
|
|
339
|
+
});
|
|
340
|
+
parent.children.splice(index, 1, ...out);
|
|
341
|
+
return [SKIP, index + out.length];
|
|
342
|
+
};
|
|
343
|
+
return (tree) => visit(tree, visitor);
|
|
344
|
+
};
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/components/Form/Checkbox.tsx
|
|
347
|
+
var Checkbox = ({ checked, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
348
|
+
...props,
|
|
349
|
+
className: clsx("str-chat__checkmark str-chat__checkbox", {
|
|
350
|
+
"str-chat__checkbox--checked": checked,
|
|
351
|
+
"str-chat__checkmark--checked": checked
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
var Checkmark = Checkbox;
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/components/ChannelListItem/utils.tsx
|
|
357
|
+
var remarkPlugins = [
|
|
358
|
+
htmlToTextPlugin,
|
|
359
|
+
[remarkGfm, { singleTilde: false }],
|
|
360
|
+
plusPlusToEmphasis,
|
|
361
|
+
imageToLink
|
|
362
|
+
];
|
|
363
|
+
var renderPreviewText = (text) => /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
364
|
+
remarkPlugins,
|
|
365
|
+
skipHtml: true,
|
|
366
|
+
children: text
|
|
367
|
+
});
|
|
368
|
+
var createPlainTextProcessor = () => unified().use(remarkParse).use(remarkPlugins);
|
|
369
|
+
var plainTextProcessor;
|
|
370
|
+
var stripMarkdownToText = (text) => {
|
|
371
|
+
try {
|
|
372
|
+
const processor = plainTextProcessor ?? (plainTextProcessor = createPlainTextProcessor());
|
|
373
|
+
return toString(processor.runSync(processor.parse(text))) || text;
|
|
374
|
+
} catch {
|
|
375
|
+
return text;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
var getLatestPollVote = (latestVotesByOption) => {
|
|
379
|
+
let latestVote;
|
|
380
|
+
for (const optionVotes of Object.values(latestVotesByOption)) optionVotes.forEach((vote) => {
|
|
381
|
+
if (latestVote && new Date(latestVote.updated_at) >= new Date(vote.created_at)) return;
|
|
382
|
+
latestVote = vote;
|
|
383
|
+
});
|
|
384
|
+
return latestVote;
|
|
385
|
+
};
|
|
386
|
+
/**
|
|
387
|
+
* Resolves the channel's latest-message preview once: the localized DISPLAY string, a `kind`
|
|
388
|
+
* discriminant, and the bits announcements need (poll question, attachment type). Single source of
|
|
389
|
+
* truth for the display variant ({@link getLatestMessagePreview}) and the announcement variant
|
|
390
|
+
* ({@link getLatestMessagePreviewText}), so the translation lookup and branching run once per call.
|
|
391
|
+
*/
|
|
392
|
+
var getLatestMessagePreviewParts = (channel, t, userLanguage = "en", latestMessageArg) => {
|
|
393
|
+
const latestMessage = latestMessageArg ?? channel.state.latestMessages[channel.state.latestMessages.length - 1];
|
|
394
|
+
const previewTextToRender = getTranslatedMessageText({
|
|
395
|
+
language: userLanguage,
|
|
396
|
+
message: latestMessage
|
|
397
|
+
}) || latestMessage?.text;
|
|
398
|
+
const poll = latestMessage?.poll;
|
|
399
|
+
if (!latestMessage) return {
|
|
400
|
+
isUserMessageText: false,
|
|
401
|
+
kind: "empty",
|
|
402
|
+
latestMessage,
|
|
403
|
+
text: t("Nothing yet...")
|
|
404
|
+
};
|
|
405
|
+
if (isMessageDeleted(latestMessage)) return {
|
|
406
|
+
isUserMessageText: false,
|
|
407
|
+
kind: "deleted",
|
|
408
|
+
latestMessage,
|
|
409
|
+
text: t("Message deleted")
|
|
410
|
+
};
|
|
411
|
+
if (poll) if (!poll.vote_count) {
|
|
412
|
+
const createdBy = poll.created_by?.id === channel.getClient().userID ? t("You") : poll.created_by?.name ?? t("Poll");
|
|
413
|
+
return {
|
|
414
|
+
isUserMessageText: false,
|
|
415
|
+
kind: "poll",
|
|
416
|
+
latestMessage,
|
|
417
|
+
pollName: poll.name,
|
|
418
|
+
text: t("📊 {{createdBy}} created: {{ pollName}}", {
|
|
419
|
+
createdBy,
|
|
420
|
+
pollName: poll.name
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
} else {
|
|
424
|
+
const latestVote = getLatestPollVote(poll.latest_votes_by_option);
|
|
425
|
+
const option = latestVote && poll.options.find((opt) => opt.id === latestVote.option_id);
|
|
426
|
+
if (option && latestVote) return {
|
|
427
|
+
isUserMessageText: false,
|
|
428
|
+
kind: "poll",
|
|
429
|
+
latestMessage,
|
|
430
|
+
pollName: poll.name,
|
|
431
|
+
text: t("📊 {{votedBy}} voted: {{pollOptionText}}", {
|
|
432
|
+
pollOptionText: option.text,
|
|
433
|
+
votedBy: latestVote?.user?.id === channel.getClient().userID ? t("You") : latestVote.user?.name ?? t("Poll")
|
|
434
|
+
})
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
if (previewTextToRender) return {
|
|
438
|
+
isUserMessageText: true,
|
|
439
|
+
kind: "text",
|
|
440
|
+
latestMessage,
|
|
441
|
+
text: previewTextToRender
|
|
442
|
+
};
|
|
443
|
+
if (latestMessage.command) return {
|
|
444
|
+
isUserMessageText: false,
|
|
445
|
+
kind: "command",
|
|
446
|
+
latestMessage,
|
|
447
|
+
text: `/${latestMessage.command}`
|
|
448
|
+
};
|
|
449
|
+
if (latestMessage.attachments?.length) return {
|
|
450
|
+
isUserMessageText: false,
|
|
451
|
+
kind: "attachment",
|
|
452
|
+
latestMessage,
|
|
453
|
+
text: t("🏙 Attachment...")
|
|
454
|
+
};
|
|
455
|
+
if (latestMessage.shared_location) return {
|
|
456
|
+
isUserMessageText: false,
|
|
457
|
+
kind: "location",
|
|
458
|
+
latestMessage,
|
|
459
|
+
text: t("📍Shared location")
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
isUserMessageText: false,
|
|
463
|
+
kind: "text",
|
|
464
|
+
latestMessage,
|
|
465
|
+
text: t("Empty message...")
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* Maps a known attachment `type` to a localized, human-readable word (e.g. "image" → "Image"). The
|
|
470
|
+
* cases are literal `t('aria/…')` calls so `i18next-cli` extracts them. Unknown/custom types return
|
|
471
|
+
* `undefined`, so the announcement falls back to a generic "Attachment".
|
|
472
|
+
*/
|
|
473
|
+
var getAttachmentTypeLabel = (type, t) => {
|
|
474
|
+
switch (type) {
|
|
475
|
+
case "audio": return t("aria/audio");
|
|
476
|
+
case "file": return t("aria/file");
|
|
477
|
+
case "giphy": return t("aria/GIF");
|
|
478
|
+
case "image": return t("aria/image");
|
|
479
|
+
case "video": return t("aria/video");
|
|
480
|
+
case "voiceRecording": return t("aria/voice message");
|
|
481
|
+
default: return;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* The text announced for the channel's latest message (for an accessible name / live region).
|
|
486
|
+
* Non-text content gets a concise, screen-reader-friendly phrasing that may differ from the visible
|
|
487
|
+
* preview: a poll announces its question, an attachment its type, a shared location and a deleted
|
|
488
|
+
* message a plain phrase. User-message text is stripped of markdown so the words are read, not the
|
|
489
|
+
* syntax (AI-generated text is returned verbatim, matching the display path).
|
|
490
|
+
*/
|
|
491
|
+
var getLatestMessagePreviewText = (channel, t, userLanguage = "en", isMessageAIGenerated, latestMessage) => {
|
|
492
|
+
const { isUserMessageText, kind, latestMessage: resolvedLatestMessage, pollName, text } = getLatestMessagePreviewParts(channel, t, userLanguage, latestMessage);
|
|
493
|
+
switch (kind) {
|
|
494
|
+
case "poll": return t("aria/Poll: {{ pollName }}", { pollName: pollName ?? "" });
|
|
495
|
+
case "attachment": {
|
|
496
|
+
const realAttachments = resolvedLatestMessage?.attachments?.filter((attachment) => !attachment.og_scrape_url) ?? [];
|
|
497
|
+
if (realAttachments.length > 1) return t("aria/Message with attachments");
|
|
498
|
+
const typeLabel = getAttachmentTypeLabel(realAttachments[0]?.type, t);
|
|
499
|
+
return typeLabel ? t("aria/Attachment {{ attachmentType }}", { attachmentType: typeLabel }) : t("aria/Attachment");
|
|
500
|
+
}
|
|
501
|
+
case "location": return t("aria/Shared location");
|
|
502
|
+
case "empty": return t("aria/There are no messages in this chat.");
|
|
503
|
+
case "text": return isUserMessageText && resolvedLatestMessage && !isMessageAIGenerated?.(resolvedLatestMessage) ? stripMarkdownToText(text) : text;
|
|
504
|
+
default: return text;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var getLatestMessagePreview = (channel, t, userLanguage = "en", isMessageAIGenerated, latestMessage) => {
|
|
508
|
+
const { isUserMessageText, latestMessage: resolvedLatestMessage, text } = getLatestMessagePreviewParts(channel, t, userLanguage, latestMessage);
|
|
509
|
+
return isUserMessageText && resolvedLatestMessage && !isMessageAIGenerated?.(resolvedLatestMessage) ? renderPreviewText(text) : text;
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* Channel display image: channel.data.image, or for DM (2 members) the other member's user.image.
|
|
513
|
+
*/
|
|
514
|
+
var getChannelDisplayImage = (channel, currentUserId) => {
|
|
515
|
+
const data = channel.data;
|
|
516
|
+
if (data?.image && typeof data.image === "string") return data.image;
|
|
517
|
+
const memberList = Object.values(channel.state.members);
|
|
518
|
+
if (memberList.length === 2) {
|
|
519
|
+
const image = memberList.find((m) => m.user?.id !== currentUserId)?.user?.image;
|
|
520
|
+
if (image && typeof image === "string") return image;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var getGroupChannelDisplayInfo = (channel) => {
|
|
524
|
+
const members = Object.values(channel.state.members);
|
|
525
|
+
if (members.length <= 2) return;
|
|
526
|
+
const memberList = [];
|
|
527
|
+
for (const member of members) {
|
|
528
|
+
const { user } = member;
|
|
529
|
+
if (!user?.name && !user?.image) continue;
|
|
530
|
+
memberList.push(extractDisplayInfo(member));
|
|
531
|
+
}
|
|
532
|
+
return { members: memberList };
|
|
533
|
+
};
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/components/ChannelListItem/hooks/useChannelDisplayName.ts
|
|
536
|
+
/**
|
|
537
|
+
* 1. channel.data.name
|
|
538
|
+
* 2. DM (exactly 2 members): other member's name, then directMessageLabel
|
|
539
|
+
* 3. Group (3+ members): comma-separated list of 2 other members' names (no ellipsis)
|
|
540
|
+
* 4. undefined otherwise
|
|
541
|
+
*/
|
|
542
|
+
function computeChannelDisplayName(channel, directMessageLabel, currentUserId) {
|
|
543
|
+
const data = channel.data;
|
|
544
|
+
if (data?.name && typeof data.name === "string") return data.name;
|
|
545
|
+
const memberList = Object.values(channel.state.members);
|
|
546
|
+
const otherMembers = memberList.filter((m) => m.user?.id !== currentUserId);
|
|
547
|
+
if (memberList.length === 2 && otherMembers.length === 1) return otherMembers[0].user?.name || directMessageLabel;
|
|
548
|
+
if (otherMembers.length >= 2) {
|
|
549
|
+
const names = otherMembers.map((m) => m.user?.name).filter(Boolean).slice(0, 2);
|
|
550
|
+
if (names.length > 0) return names.join(", ");
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Channel display name with translation context.
|
|
555
|
+
* 1. channel.data.name
|
|
556
|
+
* 2. DM (exactly 2 members): other member's name, then translated "Direct message"
|
|
557
|
+
* 3. Group (3+ members): comma-separated list of 2 other members' names (no ellipsis)
|
|
558
|
+
* 4. undefined otherwise
|
|
559
|
+
*/
|
|
560
|
+
var useChannelDisplayName = (channel) => {
|
|
561
|
+
const { client } = useChatContext("useChannelDisplayName");
|
|
562
|
+
const { t } = useTranslationContext("useChannelDisplayName");
|
|
563
|
+
const directMessageLabel = t("Direct message");
|
|
564
|
+
const [displayName, setDisplayName] = useState(() => channel ? computeChannelDisplayName(channel, directMessageLabel, client.userID ?? void 0) : void 0);
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
if (!channel) {
|
|
567
|
+
setDisplayName(void 0);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const updateDisplayName = () => setDisplayName(computeChannelDisplayName(channel, directMessageLabel, client.userID ?? void 0));
|
|
571
|
+
updateDisplayName();
|
|
572
|
+
client.on("user.updated", updateDisplayName);
|
|
573
|
+
return () => {
|
|
574
|
+
client.off("user.updated", updateDisplayName);
|
|
575
|
+
};
|
|
576
|
+
}, [
|
|
577
|
+
channel,
|
|
578
|
+
channel?.data,
|
|
579
|
+
client,
|
|
580
|
+
directMessageLabel
|
|
581
|
+
]);
|
|
582
|
+
return displayName;
|
|
583
|
+
};
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/components/ChannelListItem/hooks/useChannelPreviewInfo.ts
|
|
586
|
+
var emptyGroupInfo = {
|
|
587
|
+
members: [],
|
|
588
|
+
overflowCount: void 0
|
|
589
|
+
};
|
|
590
|
+
var useChannelPreviewInfo = (props) => {
|
|
591
|
+
const { channel, overrideImage, overrideTitle } = props;
|
|
592
|
+
const { client } = useChatContext();
|
|
593
|
+
const channelDisplayName = useChannelDisplayName(channel);
|
|
594
|
+
const displayTitle = overrideTitle ?? channelDisplayName;
|
|
595
|
+
const [displayImage, setDisplayImage] = useState(() => channel ? overrideImage ?? getChannelDisplayImage(channel, client.userID ?? void 0) : void 0);
|
|
596
|
+
const [groupChannelDisplayInfo, setGroupChannelDisplayInfo] = useState(() => channel ? getGroupChannelDisplayInfo(channel) ?? emptyGroupInfo : emptyGroupInfo);
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!channel) return;
|
|
599
|
+
if (overrideImage) return;
|
|
600
|
+
const updateInfo = () => {
|
|
601
|
+
setDisplayImage(getChannelDisplayImage(channel, client.userID ?? void 0));
|
|
602
|
+
setGroupChannelDisplayInfo(getGroupChannelDisplayInfo(channel) ?? emptyGroupInfo);
|
|
603
|
+
};
|
|
604
|
+
updateInfo();
|
|
605
|
+
const { unsubscribe: unsubscribeChannelUpdated } = channel.on("channel.updated", updateInfo);
|
|
606
|
+
const { unsubscribe: unsubscribeUserUpdated } = client.on("user.updated", updateInfo);
|
|
607
|
+
return () => {
|
|
608
|
+
unsubscribeChannelUpdated();
|
|
609
|
+
unsubscribeUserUpdated();
|
|
610
|
+
};
|
|
611
|
+
}, [
|
|
612
|
+
channel,
|
|
613
|
+
channel?.data,
|
|
614
|
+
client,
|
|
615
|
+
overrideImage
|
|
616
|
+
]);
|
|
617
|
+
return useMemo(() => ({
|
|
618
|
+
displayImage: overrideImage ?? displayImage,
|
|
619
|
+
displayTitle,
|
|
620
|
+
groupChannelDisplayInfo
|
|
621
|
+
}), [
|
|
622
|
+
displayImage,
|
|
623
|
+
displayTitle,
|
|
624
|
+
groupChannelDisplayInfo,
|
|
625
|
+
overrideImage
|
|
626
|
+
]);
|
|
627
|
+
};
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/utils/isDmChannel.ts
|
|
630
|
+
var isDmChannel = ({ channel, ownUserId }) => {
|
|
631
|
+
const memberCount = channel.data?.member_count ?? 0;
|
|
632
|
+
return memberCount === 1 || memberCount === 2 && !!ownUserId && Object.values(channel.state?.members ?? {}).some(({ user }) => user?.id === ownUserId);
|
|
633
|
+
};
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/components/ChannelList/hooks/useSelectedChannelState.ts
|
|
636
|
+
var noop = () => {};
|
|
637
|
+
function useSelectedChannelState({ channel, selector, stateChangeEventKeys = ["all"] }) {
|
|
638
|
+
return useSyncExternalStore(useCallback((onStoreChange) => {
|
|
639
|
+
if (!channel) return noop;
|
|
640
|
+
const subscriptions = stateChangeEventKeys.map((et) => channel.on(et, () => {
|
|
641
|
+
onStoreChange(selector(channel));
|
|
642
|
+
}));
|
|
643
|
+
return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
644
|
+
}, [
|
|
645
|
+
channel,
|
|
646
|
+
selector,
|
|
647
|
+
stateChangeEventKeys
|
|
648
|
+
]), useCallback(() => {
|
|
649
|
+
if (!channel) return void 0;
|
|
650
|
+
return selector(channel);
|
|
651
|
+
}, [channel, selector]));
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/components/ChannelList/hooks/useChannelMembershipState.ts
|
|
655
|
+
var selector = (c) => c.state.membership;
|
|
656
|
+
var keys = ["member.updated"];
|
|
657
|
+
function useChannelMembershipState(channel) {
|
|
658
|
+
return useSelectedChannelState({
|
|
659
|
+
channel,
|
|
660
|
+
selector,
|
|
661
|
+
stateChangeEventKeys: keys
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/components/ChannelListItem/hooks/useIsChannelMuted.ts
|
|
666
|
+
/**
|
|
667
|
+
* `channel.muteStatus()` throws in two cases:
|
|
668
|
+
* - the channel has not been initialized yet (i.e. `.watch()`/`.query()` has not
|
|
669
|
+
* resolved) - the ChannelList can briefly hold such channels, e.g. a
|
|
670
|
+
* `message.new` for a channel that has not been watched yet (#2474);
|
|
671
|
+
* - the channel is disconnected (e.g. after `client.disconnectUser()`), in which
|
|
672
|
+
* case `channel.getClient()` throws even though the channel stays initialized
|
|
673
|
+
* (#2393 failure class).
|
|
674
|
+
* Guard against both to avoid crashing the whole app.
|
|
675
|
+
*/
|
|
676
|
+
var getMuteStatus = (channel) => channel.initialized && !channel.disconnected ? channel.muteStatus() : {
|
|
677
|
+
createdAt: null,
|
|
678
|
+
expiresAt: null,
|
|
679
|
+
muted: false
|
|
680
|
+
};
|
|
681
|
+
var useIsChannelMuted = (channel) => {
|
|
682
|
+
const { client } = useChatContext("useIsChannelMuted");
|
|
683
|
+
const [muted, setMuted] = useState(() => getMuteStatus(channel));
|
|
684
|
+
useEffect(() => {
|
|
685
|
+
const handleEvent = () => {
|
|
686
|
+
setMuted(getMuteStatus(channel));
|
|
687
|
+
};
|
|
688
|
+
return client.on("notification.channel_mutes_updated", handleEvent).unsubscribe;
|
|
689
|
+
}, [channel, client]);
|
|
690
|
+
return muted;
|
|
691
|
+
};
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/components/ChannelListItem/hooks/useIsUserMuted.ts
|
|
694
|
+
var useIsUserMuted = (targetUserId) => {
|
|
695
|
+
const { mutes } = useChatContext();
|
|
696
|
+
return useMemo(() => !!targetUserId && mutes.some((mute) => mute.target.id === targetUserId), [mutes, targetUserId]);
|
|
697
|
+
};
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/components/ChannelHeader/hooks/useChannelHasMembersOnline.ts
|
|
700
|
+
var getOtherWatchers = (watchers, ownUserId) => {
|
|
701
|
+
const next = Object.assign({}, watchers ?? {});
|
|
702
|
+
if (ownUserId) delete next[ownUserId];
|
|
703
|
+
return next;
|
|
704
|
+
};
|
|
705
|
+
var useChannelHasMembersOnline = ({ channel: channelOverride, enabled = true } = {}) => {
|
|
706
|
+
const { channel: contextChannel } = useChannelStateContext();
|
|
707
|
+
const { client } = useChatContext();
|
|
708
|
+
const channel = channelOverride ?? contextChannel;
|
|
709
|
+
const ownUserId = client.user?.id;
|
|
710
|
+
const [watchers, setWatchers] = useState(() => getOtherWatchers(channel?.state?.watchers, ownUserId));
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
setWatchers(getOtherWatchers(channel?.state?.watchers, ownUserId));
|
|
713
|
+
}, [channel, ownUserId]);
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
if (!enabled || !channel) return;
|
|
716
|
+
const startSubscription = channel.on("user.watching.start", (event) => {
|
|
717
|
+
setWatchers((prev) => {
|
|
718
|
+
if (!event.user?.id || event.user.id === ownUserId) return prev;
|
|
719
|
+
if (prev[event.user.id]) return prev;
|
|
720
|
+
return Object.assign({ [event.user.id]: event.user }, prev);
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
const stopSubscription = channel.on("user.watching.stop", (event) => {
|
|
724
|
+
setWatchers((prev) => {
|
|
725
|
+
if (!event.user?.id || !prev[event.user.id]) return prev;
|
|
726
|
+
const next = Object.assign({}, prev);
|
|
727
|
+
delete next[event.user.id];
|
|
728
|
+
return next;
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
return () => {
|
|
732
|
+
startSubscription.unsubscribe();
|
|
733
|
+
stopSubscription.unsubscribe();
|
|
734
|
+
};
|
|
735
|
+
}, [
|
|
736
|
+
channel,
|
|
737
|
+
enabled,
|
|
738
|
+
ownUserId
|
|
739
|
+
]);
|
|
740
|
+
if (!enabled) return false;
|
|
741
|
+
return Object.keys(watchers).length > 0;
|
|
742
|
+
};
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts
|
|
745
|
+
/**
|
|
746
|
+
* Returns the channel header online status text (e.g. "Online", "Offline", or "X members, Y online").
|
|
747
|
+
* Returns null when the channel has no members (nothing to show).
|
|
748
|
+
*/
|
|
749
|
+
function useChannelHeaderOnlineStatus({ channel: channelOverride, watcherCount: watcherCountOverride } = {}) {
|
|
750
|
+
const { t } = useTranslationContext();
|
|
751
|
+
const { client } = useChatContext();
|
|
752
|
+
const { channel: contextChannel, watcherCount: contextWatcherCount = 0 } = useChannelStateContext();
|
|
753
|
+
const channel = channelOverride ?? contextChannel;
|
|
754
|
+
const watcherCount = watcherCountOverride ?? contextWatcherCount;
|
|
755
|
+
const { member_count: memberCount = 0 } = channel?.data || {};
|
|
756
|
+
const isDirectMessagingChannel = isDmChannel({
|
|
757
|
+
channel,
|
|
758
|
+
ownUserId: client.user?.id
|
|
759
|
+
});
|
|
760
|
+
const hasMembersOnline = useChannelHasMembersOnline({
|
|
761
|
+
channel,
|
|
762
|
+
enabled: isDirectMessagingChannel
|
|
763
|
+
});
|
|
764
|
+
if (!memberCount) return null;
|
|
765
|
+
if (isDirectMessagingChannel) return hasMembersOnline ? t("Online") : t("Offline");
|
|
766
|
+
return `${t("{{ memberCount }} members", { memberCount })} · ${t("{{ watcherCount }} online", { watcherCount })}`;
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
export { getTranslatedMessageText as A, ErrorBadge as C, useStableCallback as D, Root as E, MessageTranslationViewContext as O, Badge as S, Header as T, plusPlusToEmphasis as _, useChannelMembershipState as a, GroupAvatar as b, useChannelPreviewInfo as c, getGroupChannelDisplayInfo as d, getLatestMessagePreview as f, Checkmark as g, Checkbox as h, useIsChannelMuted as i, useMessageTranslationViewContext as j, MessageTranslationViewProvider as k, useChannelDisplayName as l, renderPreviewText as m, useChannelHasMembersOnline as n, useSelectedChannelState as o, getLatestMessagePreviewText as p, useIsUserMuted as r, isDmChannel as s, useChannelHeaderOnlineStatus as t, getChannelDisplayImage as u, imageToLink as v, Alert as w, ChannelAvatar as x, htmlToTextPlugin as y };
|
|
770
|
+
|
|
771
|
+
//# sourceMappingURL=useChannelHeaderOnlineStatus.a9357343.mjs.map
|