stream-chat-react 13.6.6 → 13.8.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/dist/components/Channel/Channel.d.ts +1 -1
- package/dist/components/Channel/Channel.js +2 -0
- package/dist/components/ChannelPreview/ChannelPreview.js +13 -2
- package/dist/components/ChannelPreview/utils.js +3 -1
- package/dist/components/Chat/hooks/useChat.js +1 -1
- package/dist/components/Message/MessageSimple.js +2 -2
- package/dist/components/Message/QuotedMessage.js +3 -1
- package/dist/components/Message/hooks/useActionHandler.js +6 -5
- package/dist/components/Message/renderText/remarkPlugins/imageToLink.d.ts +12 -0
- package/dist/components/Message/renderText/remarkPlugins/imageToLink.js +27 -0
- package/dist/components/Message/renderText/remarkPlugins/index.d.ts +2 -0
- package/dist/components/Message/renderText/remarkPlugins/index.js +2 -0
- package/dist/components/Message/renderText/remarkPlugins/plusPlusToEmphasis.d.ts +6 -0
- package/dist/components/Message/renderText/remarkPlugins/plusPlusToEmphasis.js +64 -0
- package/dist/components/Message/renderText/renderText.js +4 -1
- package/dist/components/MessageInput/EditMessageForm.d.ts +2 -1
- package/dist/components/MessageInput/hooks/useMessageComposer.js +3 -1
- package/dist/components/Poll/PollActions/PollActions.js +2 -1
- package/dist/components/TextareaComposer/TextareaComposer.js +4 -1
- package/dist/context/ComponentContext.d.ts +3 -1
- package/dist/experimental/index.browser.cjs +125 -49
- package/dist/experimental/index.browser.cjs.map +4 -4
- package/dist/experimental/index.node.cjs +125 -49
- package/dist/experimental/index.node.cjs.map +4 -4
- package/dist/i18n/TranslationBuilder/notifications/NotificationTranslationTopic.js +2 -1
- package/dist/i18n/TranslationBuilder/notifications/attachmentUpload.d.ts +1 -0
- package/dist/i18n/TranslationBuilder/notifications/attachmentUpload.js +5 -0
- package/dist/index.browser.cjs +1414 -1314
- package/dist/index.browser.cjs.map +4 -4
- package/dist/index.node.cjs +1416 -1314
- package/dist/index.node.cjs.map +4 -4
- package/dist/plugins/Emojis/index.browser.cjs +4 -1
- package/dist/plugins/Emojis/index.browser.cjs.map +2 -2
- package/dist/plugins/Emojis/index.node.cjs +4 -1
- package/dist/plugins/Emojis/index.node.cjs.map +2 -2
- package/dist/types/defaultDataInterfaces.d.ts +2 -0
- package/dist/utils/useStableCallback.d.ts +25 -0
- package/dist/utils/useStableCallback.js +29 -0
- package/package.json +3 -4
|
@@ -5,7 +5,7 @@ import type { OnMentionAction } from './hooks/useMentionsHandlers';
|
|
|
5
5
|
import type { LoadingErrorIndicatorProps } from '../Loading';
|
|
6
6
|
import type { ComponentContextValue } from '../../context';
|
|
7
7
|
import type { ChannelUnreadUiState, GiphyVersions, ImageAttachmentSizeHandler, VideoAttachmentSizeHandler } from '../../types/types';
|
|
8
|
-
type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'ShareLocationDialog' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageIsThreadReplyInChannelButtonIndicator' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'Modal' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'ReminderNotification' | 'SendButton' | 'SendToChannelCheckbox' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
|
|
8
|
+
type ChannelPropsForwardedToComponentContext = Pick<ComponentContextValue, 'Attachment' | 'AttachmentPreviewList' | 'AttachmentSelector' | 'AttachmentSelectorInitiationButtonContents' | 'AudioRecorder' | 'AutocompleteSuggestionItem' | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' | 'EditMessageInput' | 'EditMessageModal' | 'EmojiPicker' | 'emojiSearchIndex' | 'EmptyStateIndicator' | 'FileUploadIcon' | 'GiphyPreviewMessage' | 'HeaderComponent' | 'Input' | 'LinkPreviewList' | 'LoadingIndicator' | 'ShareLocationDialog' | 'Message' | 'MessageActions' | 'MessageBouncePrompt' | 'MessageBlocked' | 'MessageDeleted' | 'MessageIsThreadReplyInChannelButtonIndicator' | 'MessageListNotifications' | 'MessageListMainPanel' | 'MessageNotification' | 'MessageOptions' | 'MessageRepliesCountButton' | 'MessageStatus' | 'MessageSystem' | 'MessageTimestamp' | 'Modal' | 'ModalGallery' | 'PinIndicator' | 'PollActions' | 'PollContent' | 'PollCreationDialog' | 'PollHeader' | 'PollOptionSelector' | 'QuotedMessage' | 'QuotedMessagePreview' | 'QuotedPoll' | 'reactionOptions' | 'ReactionSelector' | 'ReactionsList' | 'ReactionsListModal' | 'ReminderNotification' | 'SendButton' | 'SendToChannelCheckbox' | 'StartRecordingAudioButton' | 'TextareaComposer' | 'ThreadHead' | 'ThreadHeader' | 'ThreadStart' | 'Timestamp' | 'TypingIndicator' | 'UnreadMessagesNotification' | 'UnreadMessagesSeparator' | 'VirtualMessage' | 'StopAIGenerationButton' | 'StreamedMessageText'>;
|
|
9
9
|
export type ChannelProps = ChannelPropsForwardedToComponentContext & {
|
|
10
10
|
/** Custom handler function that runs when the active channel has unread messages and the app is running on a separate browser tab */
|
|
11
11
|
activeUnreadHandler?: (unread: number, documentTitle: string) => void;
|
|
@@ -724,6 +724,7 @@ const ChannelInner = (props) => {
|
|
|
724
724
|
CustomMessageActionsList: props.CustomMessageActionsList,
|
|
725
725
|
DateSeparator: props.DateSeparator,
|
|
726
726
|
EditMessageInput: props.EditMessageInput,
|
|
727
|
+
EditMessageModal: props.EditMessageModal,
|
|
727
728
|
EmojiPicker: props.EmojiPicker,
|
|
728
729
|
emojiSearchIndex: props.emojiSearchIndex,
|
|
729
730
|
EmptyStateIndicator: props.EmptyStateIndicator,
|
|
@@ -791,6 +792,7 @@ const ChannelInner = (props) => {
|
|
|
791
792
|
props.CustomMessageActionsList,
|
|
792
793
|
props.DateSeparator,
|
|
793
794
|
props.EditMessageInput,
|
|
795
|
+
props.EditMessageModal,
|
|
794
796
|
props.EmojiPicker,
|
|
795
797
|
props.emojiSearchIndex,
|
|
796
798
|
props.EmptyStateIndicator,
|
|
@@ -15,6 +15,7 @@ export const ChannelPreview = (props) => {
|
|
|
15
15
|
channel,
|
|
16
16
|
});
|
|
17
17
|
const [lastMessage, setLastMessage] = useState(channel.state.messages[channel.state.messages.length - 1]);
|
|
18
|
+
const [latestMessagePreview, setLatestMessagePreview] = useState(() => getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated));
|
|
18
19
|
const [unread, setUnread] = useState(0);
|
|
19
20
|
const { messageDeliveryStatus } = useMessageDeliveryStatus({
|
|
20
21
|
channel,
|
|
@@ -55,11 +56,13 @@ export const ChannelPreview = (props) => {
|
|
|
55
56
|
}, 400), [channel, muted]);
|
|
56
57
|
useEffect(() => {
|
|
57
58
|
refreshUnreadCount();
|
|
59
|
+
setLatestMessagePreview(getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated));
|
|
58
60
|
const handleEvent = (event) => {
|
|
59
61
|
const deletedMessagesInAnotherChannel = event.type === 'user.messages.deleted' && event.cid && event.cid !== channel.cid;
|
|
60
62
|
if (deletedMessagesInAnotherChannel)
|
|
61
63
|
return;
|
|
62
64
|
setLastMessage(channel.state.latestMessages[channel.state.latestMessages.length - 1]);
|
|
65
|
+
setLatestMessagePreview(getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated));
|
|
63
66
|
refreshUnreadCount();
|
|
64
67
|
};
|
|
65
68
|
channel.on('message.new', handleEvent);
|
|
@@ -76,9 +79,17 @@ export const ChannelPreview = (props) => {
|
|
|
76
79
|
channel.off('message.undeleted', handleEvent);
|
|
77
80
|
channel.off('channel.truncated', handleEvent);
|
|
78
81
|
};
|
|
79
|
-
}, [
|
|
82
|
+
}, [
|
|
83
|
+
channel,
|
|
84
|
+
client,
|
|
85
|
+
refreshUnreadCount,
|
|
86
|
+
channelUpdateCount,
|
|
87
|
+
getLatestMessagePreview,
|
|
88
|
+
t,
|
|
89
|
+
userLanguage,
|
|
90
|
+
isMessageAIGenerated,
|
|
91
|
+
]);
|
|
80
92
|
if (!Preview)
|
|
81
93
|
return null;
|
|
82
|
-
const latestMessagePreview = getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated);
|
|
83
94
|
return (React.createElement(Preview, { ...props, active: isActive, displayImage: displayImage, displayTitle: displayTitle, groupChannelDisplayInfo: groupChannelDisplayInfo, lastMessage: lastMessage, latestMessage: latestMessagePreview, latestMessagePreview: latestMessagePreview, messageDeliveryStatus: messageDeliveryStatus, setActiveChannel: setActiveChannel, unread: unread }));
|
|
84
95
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import ReactMarkdown from 'react-markdown';
|
|
3
|
-
import { htmlToTextPlugin } from '../Message';
|
|
3
|
+
import { htmlToTextPlugin, imageToLink, plusPlusToEmphasis } from '../Message';
|
|
4
4
|
import remarkGfm from 'remark-gfm';
|
|
5
5
|
const remarkPlugins = [
|
|
6
6
|
htmlToTextPlugin,
|
|
7
7
|
[remarkGfm, { singleTilde: false }],
|
|
8
|
+
plusPlusToEmphasis,
|
|
9
|
+
imageToLink,
|
|
8
10
|
];
|
|
9
11
|
export const renderPreviewText = (text) => (React.createElement(ReactMarkdown, { remarkPlugins: remarkPlugins, skipHtml: true }, text));
|
|
10
12
|
const getLatestPollVote = (latestVotesByOption) => {
|
|
@@ -24,7 +24,7 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
|
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
if (!client)
|
|
26
26
|
return;
|
|
27
|
-
const version = "13.
|
|
27
|
+
const version = "13.8.0";
|
|
28
28
|
const userAgent = client.getUserAgent();
|
|
29
29
|
if (!userAgent.includes('stream-chat-react')) {
|
|
30
30
|
// result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
|
|
@@ -17,7 +17,7 @@ import { useMessageReminder } from './hooks';
|
|
|
17
17
|
import { areMessageUIPropsEqual, isMessageBlocked, isMessageBounced, isMessageEdited, messageHasAttachments, messageHasReactions, } from './utils';
|
|
18
18
|
import { Avatar as DefaultAvatar } from '../Avatar';
|
|
19
19
|
import { Attachment as DefaultAttachment } from '../Attachment';
|
|
20
|
-
import { EditMessageModal } from '../MessageInput';
|
|
20
|
+
import { EditMessageModal as DefaultEditMessageModal } from '../MessageInput';
|
|
21
21
|
import { Poll } from '../Poll';
|
|
22
22
|
import { ReactionsList as DefaultReactionList } from '../Reactions';
|
|
23
23
|
import { MessageBounceModal } from '../MessageBounce/MessageBounceModal';
|
|
@@ -32,7 +32,7 @@ const MessageSimpleWithContext = (props) => {
|
|
|
32
32
|
const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false);
|
|
33
33
|
const [isEditedTimestampOpen, setEditedTimestampOpen] = useState(false);
|
|
34
34
|
const reminder = useMessageReminder(message.id);
|
|
35
|
-
const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, MessageOptions = DefaultMessageOptions,
|
|
35
|
+
const { Attachment = DefaultAttachment, Avatar = DefaultAvatar, EditMessageModal = DefaultEditMessageModal, MessageOptions = DefaultMessageOptions,
|
|
36
36
|
// TODO: remove this "passthrough" in the next
|
|
37
37
|
// major release and use the new default instead
|
|
38
38
|
MessageActions = MessageOptions, MessageBlocked = DefaultMessageBlocked, MessageBouncePrompt = DefaultMessageBouncePrompt, MessageDeleted = DefaultMessageDeleted, MessageIsThreadReplyInChannelButtonIndicator = DefaultMessageIsThreadReplyInChannelButtonIndicator, MessageRepliesCountButton = DefaultMessageRepliesCountButton, MessageStatus = DefaultMessageStatus, MessageTimestamp = DefaultMessageTimestamp, ReactionsList = DefaultReactionList, ReminderNotification = DefaultReminderNotification, StreamedMessageText = DefaultStreamedMessageText, PinIndicator, } = useComponentContext('MessageSimple');
|
|
@@ -9,12 +9,14 @@ import { useMessageContext } from '../../context/MessageContext';
|
|
|
9
9
|
import { useTranslationContext } from '../../context/TranslationContext';
|
|
10
10
|
import { useChannelActionContext } from '../../context/ChannelActionContext';
|
|
11
11
|
import { renderText as defaultRenderText } from './renderText';
|
|
12
|
+
import { useActionHandler } from './';
|
|
12
13
|
export const QuotedMessage = ({ renderText: propsRenderText }) => {
|
|
13
14
|
const { Attachment = DefaultAttachment, Avatar: ContextAvatar } = useComponentContext('QuotedMessage');
|
|
14
15
|
const { client } = useChatContext();
|
|
15
16
|
const { isMyMessage, message, renderText: contextRenderText, } = useMessageContext('QuotedMessage');
|
|
16
17
|
const { t, userLanguage } = useTranslationContext('QuotedMessage');
|
|
17
18
|
const { jumpToMessage } = useChannelActionContext('QuotedMessage');
|
|
19
|
+
const actionHandler = useActionHandler(message);
|
|
18
20
|
const renderText = propsRenderText ?? contextRenderText ?? defaultRenderText;
|
|
19
21
|
const Avatar = ContextAvatar || DefaultAvatar;
|
|
20
22
|
const { quoted_message } = message;
|
|
@@ -42,5 +44,5 @@ export const QuotedMessage = ({ renderText: propsRenderText }) => {
|
|
|
42
44
|
React.createElement("div", { className: 'str-chat__quoted-message-bubble', "data-testid": 'quoted-message-contents' }, poll ? (React.createElement(Poll, { isQuoted: true, poll: poll })) : (React.createElement(React.Fragment, null,
|
|
43
45
|
quotedMessageAttachment && (React.createElement(Attachment, { attachments: [quotedMessageAttachment], isQuoted: true })),
|
|
44
46
|
React.createElement("div", { className: 'str-chat__quoted-message-bubble__text', "data-testid": 'quoted-message-text' }, renderedText))))),
|
|
45
|
-
message.attachments?.length ? (React.createElement(Attachment, { attachments: message.attachments })) : null));
|
|
47
|
+
message.attachments?.length ? (React.createElement(Attachment, { actionHandler: actionHandler, attachments: message.attachments })) : null));
|
|
46
48
|
};
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { useChannelActionContext } from '../../../context/ChannelActionContext';
|
|
2
2
|
import { useChannelStateContext } from '../../../context/ChannelStateContext';
|
|
3
|
+
import { useStableCallback } from '../../../utils/useStableCallback';
|
|
3
4
|
export const handleActionWarning = `Action handler was called, but it is missing one of its required arguments.
|
|
4
5
|
Make sure the ChannelAction and ChannelState contexts are properly set and the hook is initialized with a valid message.`;
|
|
5
6
|
export function useActionHandler(message) {
|
|
6
7
|
const { removeMessage, updateMessage } = useChannelActionContext('useActionHandler');
|
|
7
8
|
const { channel } = useChannelStateContext('useActionHandler');
|
|
8
|
-
return async (dataOrName, value, event) => {
|
|
9
|
+
return useStableCallback(async (dataOrName, value, event) => {
|
|
9
10
|
if (event)
|
|
10
11
|
event.preventDefault();
|
|
11
12
|
if (!message || !updateMessage || !removeMessage || !channel) {
|
|
12
13
|
console.warn(handleActionWarning);
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
const
|
|
16
|
+
const messageId = message.id;
|
|
16
17
|
let formData = {};
|
|
17
18
|
// deprecated: value&name should be removed in favor of data obj
|
|
18
19
|
if (typeof dataOrName === 'string') {
|
|
@@ -21,8 +22,8 @@ export function useActionHandler(message) {
|
|
|
21
22
|
else {
|
|
22
23
|
formData = { ...dataOrName };
|
|
23
24
|
}
|
|
24
|
-
if (
|
|
25
|
-
const data = await channel.sendAction(
|
|
25
|
+
if (messageId) {
|
|
26
|
+
const data = await channel.sendAction(messageId, formData);
|
|
26
27
|
if (data?.message) {
|
|
27
28
|
updateMessage(data.message);
|
|
28
29
|
}
|
|
@@ -30,5 +31,5 @@ export function useActionHandler(message) {
|
|
|
30
31
|
removeMessage(message);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
};
|
|
34
|
+
});
|
|
34
35
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Node } from 'unist';
|
|
2
|
+
export type ImageToLinkPluginOptions = {
|
|
3
|
+
getTextLabelFrom?: 'alt' | 'title' | 'url';
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Converts image Markdown links ()
|
|
7
|
+
* to HTML <a href={url}>{url | title | alt}</a>
|
|
8
|
+
*
|
|
9
|
+
* By default, the anchor text content is the image url so that image preview can be generated / enriched on the server.
|
|
10
|
+
* @param getTextLabelFrom
|
|
11
|
+
*/
|
|
12
|
+
export declare function imageToLink({ getTextLabelFrom }?: ImageToLinkPluginOptions): (tree: Node) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
2
|
+
const text = (value) => ({ type: 'text', value });
|
|
3
|
+
/**
|
|
4
|
+
* Converts image Markdown links ()
|
|
5
|
+
* to HTML <a href={url}>{url | title | alt}</a>
|
|
6
|
+
*
|
|
7
|
+
* By default, the anchor text content is the image url so that image preview can be generated / enriched on the server.
|
|
8
|
+
* @param getTextLabelFrom
|
|
9
|
+
*/
|
|
10
|
+
export function imageToLink({ getTextLabelFrom = 'url' } = {}) {
|
|
11
|
+
return (tree) => {
|
|
12
|
+
const visitor = (node, index, parent) => {
|
|
13
|
+
if (parent == null || index == null)
|
|
14
|
+
return;
|
|
15
|
+
const label = node[getTextLabelFrom] ?? node.url; // node.alt || node.title || node.url;
|
|
16
|
+
const link = {
|
|
17
|
+
children: [text(label)],
|
|
18
|
+
title: node.title ?? node.alt ?? node.url,
|
|
19
|
+
type: 'link',
|
|
20
|
+
url: node.url,
|
|
21
|
+
};
|
|
22
|
+
parent.children.splice(index, 1, link);
|
|
23
|
+
return [SKIP, index + 1];
|
|
24
|
+
};
|
|
25
|
+
visit(tree, 'image', visitor);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Plugin } from 'unified';
|
|
2
|
+
/**
|
|
3
|
+
* Converts MD "++Some text++" to inserted text element rendered in HTML as <ins>Some text</ins>
|
|
4
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins
|
|
5
|
+
*/
|
|
6
|
+
export declare const plusPlusToEmphasis: Plugin<[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SKIP, visit } from 'unist-util-visit';
|
|
2
|
+
/**
|
|
3
|
+
* \S → first char must be non-whitespace
|
|
4
|
+
* (?:...)?→ optional middle+closing when length > 1
|
|
5
|
+
* [\s\S]*?→ anything (including newlines), lazy
|
|
6
|
+
* final \S→ last char non-whitespace (only required when there’s more than 1)
|
|
7
|
+
*
|
|
8
|
+
* Matches:
|
|
9
|
+
* ++a++
|
|
10
|
+
* Does not match:
|
|
11
|
+
* ++++
|
|
12
|
+
* ++ ++
|
|
13
|
+
*/
|
|
14
|
+
const INS_REGEX = /\+\+(\S(?:[\s\S]*?\S)?)\+\+/g;
|
|
15
|
+
const IGNORE_NODE_TYPES = new Set([
|
|
16
|
+
'code',
|
|
17
|
+
'inlineCode',
|
|
18
|
+
'link',
|
|
19
|
+
'linkReference',
|
|
20
|
+
'definition',
|
|
21
|
+
'math',
|
|
22
|
+
'inlineMath',
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Converts MD "++Some text++" to inserted text element rendered in HTML as <ins>Some text</ins>
|
|
26
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/ins
|
|
27
|
+
*/
|
|
28
|
+
export const plusPlusToEmphasis = () => {
|
|
29
|
+
const visitor = (node, index, parent) => {
|
|
30
|
+
// 1) Don’t traverse inside ignored nodes
|
|
31
|
+
if (IGNORE_NODE_TYPES.has(node.type))
|
|
32
|
+
return SKIP;
|
|
33
|
+
// 2) Only transform text nodes with a valid parent + index
|
|
34
|
+
if (node.type !== 'text' || parent == null || typeof index !== 'number')
|
|
35
|
+
return;
|
|
36
|
+
const value = node.value;
|
|
37
|
+
// Reset lastIndex to 0 per node so each node is scanned from the beginning
|
|
38
|
+
INS_REGEX.lastIndex = 0;
|
|
39
|
+
let match;
|
|
40
|
+
let last = 0;
|
|
41
|
+
const out = [];
|
|
42
|
+
while ((match = INS_REGEX.exec(value))) {
|
|
43
|
+
const [full, inner] = match;
|
|
44
|
+
const start = match.index;
|
|
45
|
+
if (start > last)
|
|
46
|
+
out.push({ type: 'text', value: value.slice(last, start) });
|
|
47
|
+
// Render as <ins>…</ins> (remark-rehype respects data.hName)
|
|
48
|
+
out.push({
|
|
49
|
+
children: [{ type: 'text', value: inner }],
|
|
50
|
+
data: { hName: 'ins' },
|
|
51
|
+
type: 'emphasis',
|
|
52
|
+
});
|
|
53
|
+
last = start + full.length;
|
|
54
|
+
}
|
|
55
|
+
if (out.length === 0)
|
|
56
|
+
return; // nothing to change
|
|
57
|
+
if (last < value.length)
|
|
58
|
+
out.push({ type: 'text', value: value.slice(last) });
|
|
59
|
+
parent.children.splice(index, 1, ...out);
|
|
60
|
+
// Skip re-visiting the replaced range; continue after inserted nodes
|
|
61
|
+
return [SKIP, index + out.length];
|
|
62
|
+
};
|
|
63
|
+
return (tree) => visit(tree, visitor);
|
|
64
|
+
};
|
|
@@ -5,7 +5,7 @@ import remarkGfm from 'remark-gfm';
|
|
|
5
5
|
import { Anchor, Emoji, Mention } from './componentRenderers';
|
|
6
6
|
import { detectHttp, matchMarkdownLinks, messageCodeBlocks } from './regex';
|
|
7
7
|
import { emojiMarkdownPlugin, mentionsMarkdownPlugin } from './rehypePlugins';
|
|
8
|
-
import { htmlToTextPlugin, keepLineBreaksPlugin } from './remarkPlugins';
|
|
8
|
+
import { htmlToTextPlugin, imageToLink, keepLineBreaksPlugin, plusPlusToEmphasis, } from './remarkPlugins';
|
|
9
9
|
import { ErrorBoundary } from '../../UtilityComponents';
|
|
10
10
|
export const defaultAllowedTagNames = [
|
|
11
11
|
'html',
|
|
@@ -38,6 +38,7 @@ export const defaultAllowedTagNames = [
|
|
|
38
38
|
'h4',
|
|
39
39
|
'h5',
|
|
40
40
|
'h6',
|
|
41
|
+
'ins',
|
|
41
42
|
];
|
|
42
43
|
function formatUrlForDisplay(url) {
|
|
43
44
|
try {
|
|
@@ -124,6 +125,8 @@ export const renderText = (text, mentionedUsers, { allowedTagNames = defaultAllo
|
|
|
124
125
|
htmlToTextPlugin,
|
|
125
126
|
keepLineBreaksPlugin,
|
|
126
127
|
[remarkGfm, { singleTilde: false }],
|
|
128
|
+
plusPlusToEmphasis,
|
|
129
|
+
imageToLink,
|
|
127
130
|
];
|
|
128
131
|
const rehypePlugins = [emojiMarkdownPlugin];
|
|
129
132
|
if (mentionedUsers?.length) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { MessageUIComponentProps } from '../Message';
|
|
3
3
|
export declare const EditMessageForm: () => React.JSX.Element;
|
|
4
|
-
export
|
|
4
|
+
export type EditMessageModalProps = Pick<MessageUIComponentProps, 'additionalMessageInputProps'>;
|
|
5
|
+
export declare const EditMessageModal: ({ additionalMessageInputProps, }: EditMessageModalProps) => React.JSX.Element;
|
|
@@ -30,8 +30,10 @@ export const useMessageComposer = () => {
|
|
|
30
30
|
if (editing && cachedEditedMessage) {
|
|
31
31
|
const tag = MessageComposer.constructTag(cachedEditedMessage);
|
|
32
32
|
const cachedComposer = queueCache.get(tag);
|
|
33
|
-
if (cachedComposer)
|
|
33
|
+
if (cachedComposer) {
|
|
34
|
+
cachedComposer.editedMessage = cachedEditedMessage;
|
|
34
35
|
return cachedComposer;
|
|
36
|
+
}
|
|
35
37
|
return new MessageComposer({
|
|
36
38
|
client,
|
|
37
39
|
composition: cachedEditedMessage,
|
|
@@ -28,6 +28,7 @@ export const PollActions = ({ AddCommentForm = DefaultAddCommentForm, EndPollDia
|
|
|
28
28
|
const { poll } = usePollContext();
|
|
29
29
|
const { allow_answers, allow_user_suggested_options, answers_count, created_by_id, is_closed, options, ownAnswer, } = useStateStore(poll.state, pollStateSelector);
|
|
30
30
|
const [modalOpen, setModalOpen] = useState();
|
|
31
|
+
const canCastVote = channelCapabilities['cast-poll-vote'] && !is_closed;
|
|
31
32
|
const closeModal = useCallback(() => setModalOpen(undefined), []);
|
|
32
33
|
const onUpdateAnswerClick = useCallback(() => setModalOpen('add-comment'), []);
|
|
33
34
|
return (React.createElement("div", { className: 'str-chat__poll-actions' },
|
|
@@ -35,7 +36,7 @@ export const PollActions = ({ AddCommentForm = DefaultAddCommentForm, EndPollDia
|
|
|
35
36
|
count: options.length,
|
|
36
37
|
}), closeModal: closeModal, modalClassName: COMMON_MODAL_CLASS, modalIsOpen: modalOpen === 'view-all-options', openModal: () => setModalOpen('view-all-options') },
|
|
37
38
|
React.createElement(PollOptionsFullList, { close: closeModal }))),
|
|
38
|
-
|
|
39
|
+
canCastVote &&
|
|
39
40
|
allow_user_suggested_options &&
|
|
40
41
|
options.length < MAX_POLL_OPTIONS && (React.createElement(PollAction, { buttonText: t('Suggest an option'), closeModal: closeModal, modalClassName: clsx(COMMON_MODAL_CLASS, 'str-chat__suggest-poll-option-modal'), modalIsOpen: modalOpen === 'suggest-option', openModal: () => setModalOpen('suggest-option') },
|
|
41
42
|
React.createElement(SuggestPollOptionForm, { close: closeModal, messageId: message.id }))),
|
|
@@ -110,7 +110,9 @@ export const TextareaComposer = ({ className, closeSuggestionsOnClickOutside, co
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
else if (shouldSubmit(event) &&
|
|
113
|
+
else if (shouldSubmit(event) &&
|
|
114
|
+
textareaRef.current &&
|
|
115
|
+
messageComposer.hasSendableData) {
|
|
114
116
|
if (event.key === 'Enter') {
|
|
115
117
|
// prevent adding newline when submitting a message with
|
|
116
118
|
event.preventDefault();
|
|
@@ -120,6 +122,7 @@ export const TextareaComposer = ({ className, closeSuggestionsOnClickOutside, co
|
|
|
120
122
|
}, [
|
|
121
123
|
focusedItemIndex,
|
|
122
124
|
handleSubmit,
|
|
125
|
+
messageComposer,
|
|
123
126
|
onKeyDown,
|
|
124
127
|
shouldSubmit,
|
|
125
128
|
suggestions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PropsWithChildren } from 'react';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type { AttachmentPreviewListProps, AttachmentProps, AvatarProps, BaseImageProps, ChannelPreviewActionButtonsProps, CooldownTimerProps, CustomMessageActionsListProps, DateSeparatorProps, EmojiSearchIndex, EmptyStateIndicatorProps, EventComponentProps, FixedHeightMessageProps, GiphyPreviewMessageProps, LoadingIndicatorProps, MessageBouncePromptProps, MessageDeletedProps, MessageInputProps, MessageListNotificationsProps, MessageNotificationProps, MessageOptionsProps, MessageProps, MessageRepliesCountButtonProps, MessageStatusProps, MessageTimestampProps, MessageUIComponentProps, ModalGalleryProps, ModalProps, PinIndicatorProps, PollCreationDialogProps, PollOptionSelectorProps, QuotedMessagePreviewProps, ReactionOptions, ReactionSelectorProps, ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, ReminderNotificationProps, SendButtonProps, StartRecordingAudioButtonProps, StreamedMessageTextProps, TextareaComposerProps, ThreadHeaderProps, ThreadListItemProps, ThreadListItemUIProps, TimestampProps, TypingIndicatorProps, UnreadMessagesNotificationProps, UnreadMessagesSeparatorProps } from '../components';
|
|
3
|
+
import type { AttachmentPreviewListProps, AttachmentProps, AvatarProps, BaseImageProps, ChannelPreviewActionButtonsProps, CooldownTimerProps, CustomMessageActionsListProps, DateSeparatorProps, EditMessageModalProps, EmojiSearchIndex, EmptyStateIndicatorProps, EventComponentProps, FixedHeightMessageProps, GiphyPreviewMessageProps, LoadingIndicatorProps, MessageBouncePromptProps, MessageDeletedProps, MessageInputProps, MessageListNotificationsProps, MessageNotificationProps, MessageOptionsProps, MessageProps, MessageRepliesCountButtonProps, MessageStatusProps, MessageTimestampProps, MessageUIComponentProps, ModalGalleryProps, ModalProps, PinIndicatorProps, PollCreationDialogProps, PollOptionSelectorProps, QuotedMessagePreviewProps, ReactionOptions, ReactionSelectorProps, ReactionsListModalProps, ReactionsListProps, RecordingPermissionDeniedNotificationProps, ReminderNotificationProps, SendButtonProps, StartRecordingAudioButtonProps, StreamedMessageTextProps, TextareaComposerProps, ThreadHeaderProps, ThreadListItemProps, ThreadListItemUIProps, TimestampProps, TypingIndicatorProps, UnreadMessagesNotificationProps, UnreadMessagesSeparatorProps } from '../components';
|
|
4
4
|
import type { SuggestionItemProps, SuggestionListProps } from '../components/TextareaComposer';
|
|
5
5
|
import type { SearchProps, SearchResultsPresearchProps, SearchSourceResultListProps } from '../experimental';
|
|
6
6
|
import type { PropsWithChildrenOnly, UnknownType } from '../types/types';
|
|
@@ -35,6 +35,8 @@ export type ComponentContextValue = {
|
|
|
35
35
|
DateSeparator?: React.ComponentType<DateSeparatorProps>;
|
|
36
36
|
/** Custom UI component to override default edit message input, defaults to and accepts same props as: [EditMessageForm](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/EditMessageForm.tsx) */
|
|
37
37
|
EditMessageInput?: React.ComponentType<MessageInputProps>;
|
|
38
|
+
/** Custom UI component to override default EditMessageModal, defaults to and accepts same props as: [EditMessageModal](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/EditMessageForm.tsx) */
|
|
39
|
+
EditMessageModal?: React.ComponentType<EditMessageModalProps>;
|
|
38
40
|
/** Custom UI component for rendering button with emoji picker in MessageInput */
|
|
39
41
|
EmojiPicker?: React.ComponentType;
|
|
40
42
|
/** Mechanism to be used with autocomplete and text replace features of the `MessageInput` component, see [emoji-mart `SearchIndex`](https://github.com/missive/emoji-mart#%EF%B8%8F%EF%B8%8F-headless-search) */
|