stream-chat-react-native-core 5.43.3-beta.2 → 5.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/lib/commonjs/components/AITypingIndicatorView/AITypingIndicatorView.js +53 -0
  2. package/lib/commonjs/components/AITypingIndicatorView/AITypingIndicatorView.js.map +1 -0
  3. package/lib/commonjs/components/AITypingIndicatorView/hooks/useAIState.js +59 -0
  4. package/lib/commonjs/components/AITypingIndicatorView/hooks/useAIState.js.map +1 -0
  5. package/lib/commonjs/components/AITypingIndicatorView/index.js +26 -0
  6. package/lib/commonjs/components/AITypingIndicatorView/index.js.map +1 -0
  7. package/lib/commonjs/components/Channel/Channel.js +9 -1
  8. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  9. package/lib/commonjs/components/Channel/hooks/useCreateInputMessageInputContext.js +2 -0
  10. package/lib/commonjs/components/Channel/hooks/useCreateInputMessageInputContext.js.map +1 -1
  11. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js +2 -0
  12. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  13. package/lib/commonjs/components/Message/Message.js +3 -1
  14. package/lib/commonjs/components/Message/Message.js.map +1 -1
  15. package/lib/commonjs/components/Message/MessageSimple/MessageContent.js +8 -3
  16. package/lib/commonjs/components/Message/MessageSimple/MessageContent.js.map +1 -1
  17. package/lib/commonjs/components/Message/MessageSimple/MessageFooter.js +5 -4
  18. package/lib/commonjs/components/Message/MessageSimple/MessageFooter.js.map +1 -1
  19. package/lib/commonjs/components/Message/MessageSimple/MessageSimple.js +1 -1
  20. package/lib/commonjs/components/Message/MessageSimple/MessageSimple.js.map +1 -1
  21. package/lib/commonjs/components/Message/MessageSimple/StreamingMessageView.js +36 -0
  22. package/lib/commonjs/components/Message/MessageSimple/StreamingMessageView.js.map +1 -0
  23. package/lib/commonjs/components/Message/MessageSimple/utils/generateMarkdownText.js +5 -1
  24. package/lib/commonjs/components/Message/MessageSimple/utils/generateMarkdownText.js.map +1 -1
  25. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js +209 -23
  26. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  27. package/lib/commonjs/components/Message/hooks/useStreamingMessage.js +43 -0
  28. package/lib/commonjs/components/Message/hooks/useStreamingMessage.js.map +1 -0
  29. package/lib/commonjs/components/MessageInput/MessageInput.js +20 -6
  30. package/lib/commonjs/components/MessageInput/MessageInput.js.map +1 -1
  31. package/lib/commonjs/components/MessageInput/StopMessageStreamingButton.js +39 -0
  32. package/lib/commonjs/components/MessageInput/StopMessageStreamingButton.js.map +1 -0
  33. package/lib/commonjs/components/MessageOverlay/MessageOverlay.js +6 -1
  34. package/lib/commonjs/components/MessageOverlay/MessageOverlay.js.map +1 -1
  35. package/lib/commonjs/components/index.js +44 -0
  36. package/lib/commonjs/components/index.js.map +1 -1
  37. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +4 -2
  38. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  39. package/lib/commonjs/contexts/messageInputContext/hooks/useCreateMessageInputContext.js +2 -0
  40. package/lib/commonjs/contexts/messageInputContext/hooks/useCreateMessageInputContext.js.map +1 -1
  41. package/lib/commonjs/contexts/messagesContext/MessagesContext.js.map +1 -1
  42. package/lib/commonjs/contexts/themeContext/utils/theme.js +7 -0
  43. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  44. package/lib/commonjs/i18n/en.json +2 -0
  45. package/lib/commonjs/i18n/es.json +2 -0
  46. package/lib/commonjs/i18n/fr.json +2 -0
  47. package/lib/commonjs/i18n/he.json +2 -0
  48. package/lib/commonjs/i18n/hi.json +2 -0
  49. package/lib/commonjs/i18n/it.json +2 -0
  50. package/lib/commonjs/i18n/ja.json +2 -0
  51. package/lib/commonjs/i18n/ko.json +2 -0
  52. package/lib/commonjs/i18n/nl.json +2 -0
  53. package/lib/commonjs/i18n/pt-br.json +2 -0
  54. package/lib/commonjs/i18n/ru.json +2 -0
  55. package/lib/commonjs/i18n/tr.json +2 -0
  56. package/lib/commonjs/utils/utils.js +1 -1
  57. package/lib/commonjs/utils/utils.js.map +1 -1
  58. package/lib/commonjs/version.json +1 -1
  59. package/lib/module/components/AITypingIndicatorView/AITypingIndicatorView.js +53 -0
  60. package/lib/module/components/AITypingIndicatorView/AITypingIndicatorView.js.map +1 -0
  61. package/lib/module/components/AITypingIndicatorView/hooks/useAIState.js +59 -0
  62. package/lib/module/components/AITypingIndicatorView/hooks/useAIState.js.map +1 -0
  63. package/lib/module/components/AITypingIndicatorView/index.js +26 -0
  64. package/lib/module/components/AITypingIndicatorView/index.js.map +1 -0
  65. package/lib/module/components/Channel/Channel.js +9 -1
  66. package/lib/module/components/Channel/Channel.js.map +1 -1
  67. package/lib/module/components/Channel/hooks/useCreateInputMessageInputContext.js +2 -0
  68. package/lib/module/components/Channel/hooks/useCreateInputMessageInputContext.js.map +1 -1
  69. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js +2 -0
  70. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  71. package/lib/module/components/Message/Message.js +3 -1
  72. package/lib/module/components/Message/Message.js.map +1 -1
  73. package/lib/module/components/Message/MessageSimple/MessageContent.js +8 -3
  74. package/lib/module/components/Message/MessageSimple/MessageContent.js.map +1 -1
  75. package/lib/module/components/Message/MessageSimple/MessageFooter.js +5 -4
  76. package/lib/module/components/Message/MessageSimple/MessageFooter.js.map +1 -1
  77. package/lib/module/components/Message/MessageSimple/MessageSimple.js +1 -1
  78. package/lib/module/components/Message/MessageSimple/MessageSimple.js.map +1 -1
  79. package/lib/module/components/Message/MessageSimple/StreamingMessageView.js +36 -0
  80. package/lib/module/components/Message/MessageSimple/StreamingMessageView.js.map +1 -0
  81. package/lib/module/components/Message/MessageSimple/utils/generateMarkdownText.js +5 -1
  82. package/lib/module/components/Message/MessageSimple/utils/generateMarkdownText.js.map +1 -1
  83. package/lib/module/components/Message/MessageSimple/utils/renderText.js +209 -23
  84. package/lib/module/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  85. package/lib/module/components/Message/hooks/useStreamingMessage.js +43 -0
  86. package/lib/module/components/Message/hooks/useStreamingMessage.js.map +1 -0
  87. package/lib/module/components/MessageInput/MessageInput.js +20 -6
  88. package/lib/module/components/MessageInput/MessageInput.js.map +1 -1
  89. package/lib/module/components/MessageInput/StopMessageStreamingButton.js +39 -0
  90. package/lib/module/components/MessageInput/StopMessageStreamingButton.js.map +1 -0
  91. package/lib/module/components/MessageOverlay/MessageOverlay.js +6 -1
  92. package/lib/module/components/MessageOverlay/MessageOverlay.js.map +1 -1
  93. package/lib/module/components/index.js +44 -0
  94. package/lib/module/components/index.js.map +1 -1
  95. package/lib/module/contexts/messageInputContext/MessageInputContext.js +4 -2
  96. package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  97. package/lib/module/contexts/messageInputContext/hooks/useCreateMessageInputContext.js +2 -0
  98. package/lib/module/contexts/messageInputContext/hooks/useCreateMessageInputContext.js.map +1 -1
  99. package/lib/module/contexts/messagesContext/MessagesContext.js.map +1 -1
  100. package/lib/module/contexts/themeContext/utils/theme.js +7 -0
  101. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  102. package/lib/module/i18n/en.json +2 -0
  103. package/lib/module/i18n/es.json +2 -0
  104. package/lib/module/i18n/fr.json +2 -0
  105. package/lib/module/i18n/he.json +2 -0
  106. package/lib/module/i18n/hi.json +2 -0
  107. package/lib/module/i18n/it.json +2 -0
  108. package/lib/module/i18n/ja.json +2 -0
  109. package/lib/module/i18n/ko.json +2 -0
  110. package/lib/module/i18n/nl.json +2 -0
  111. package/lib/module/i18n/pt-br.json +2 -0
  112. package/lib/module/i18n/ru.json +2 -0
  113. package/lib/module/i18n/tr.json +2 -0
  114. package/lib/module/utils/utils.js +1 -1
  115. package/lib/module/utils/utils.js.map +1 -1
  116. package/lib/module/version.json +1 -1
  117. package/lib/typescript/components/AITypingIndicatorView/AITypingIndicatorView.d.ts +11 -0
  118. package/lib/typescript/components/AITypingIndicatorView/AITypingIndicatorView.d.ts.map +1 -0
  119. package/lib/typescript/components/AITypingIndicatorView/hooks/useAIState.d.ts +18 -0
  120. package/lib/typescript/components/AITypingIndicatorView/hooks/useAIState.d.ts.map +1 -0
  121. package/lib/typescript/components/AITypingIndicatorView/index.d.ts +3 -0
  122. package/lib/typescript/components/AITypingIndicatorView/index.d.ts.map +1 -0
  123. package/lib/typescript/components/Channel/Channel.d.ts +2 -2
  124. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  125. package/lib/typescript/components/Channel/hooks/useCreateInputMessageInputContext.d.ts +1 -1
  126. package/lib/typescript/components/Channel/hooks/useCreateInputMessageInputContext.d.ts.map +1 -1
  127. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts +1 -1
  128. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts.map +1 -1
  129. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  130. package/lib/typescript/components/Message/MessageSimple/MessageContent.d.ts +1 -1
  131. package/lib/typescript/components/Message/MessageSimple/MessageContent.d.ts.map +1 -1
  132. package/lib/typescript/components/Message/MessageSimple/MessageFooter.d.ts.map +1 -1
  133. package/lib/typescript/components/Message/MessageSimple/MessageSimple.d.ts.map +1 -1
  134. package/lib/typescript/components/Message/MessageSimple/StreamingMessageView.d.ts +12 -0
  135. package/lib/typescript/components/Message/MessageSimple/StreamingMessageView.d.ts.map +1 -0
  136. package/lib/typescript/components/Message/MessageSimple/utils/generateMarkdownText.d.ts.map +1 -1
  137. package/lib/typescript/components/Message/MessageSimple/utils/renderText.d.ts +16 -1
  138. package/lib/typescript/components/Message/MessageSimple/utils/renderText.d.ts.map +1 -1
  139. package/lib/typescript/components/Message/hooks/useStreamingMessage.d.ts +17 -0
  140. package/lib/typescript/components/Message/hooks/useStreamingMessage.d.ts.map +1 -0
  141. package/lib/typescript/components/MessageInput/MessageInput.d.ts +1 -1
  142. package/lib/typescript/components/MessageInput/MessageInput.d.ts.map +1 -1
  143. package/lib/typescript/components/MessageInput/StopMessageStreamingButton.d.ts +10 -0
  144. package/lib/typescript/components/MessageInput/StopMessageStreamingButton.d.ts.map +1 -0
  145. package/lib/typescript/components/MessageOverlay/MessageOverlay.d.ts.map +1 -1
  146. package/lib/typescript/components/index.d.ts +4 -0
  147. package/lib/typescript/components/index.d.ts.map +1 -1
  148. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts +3 -2
  149. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts.map +1 -1
  150. package/lib/typescript/contexts/messageInputContext/hooks/useCreateMessageInputContext.d.ts +1 -1
  151. package/lib/typescript/contexts/messageInputContext/hooks/useCreateMessageInputContext.d.ts.map +1 -1
  152. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts +6 -2
  153. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts.map +1 -1
  154. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +8 -1
  155. package/lib/typescript/contexts/themeContext/utils/theme.d.ts.map +1 -1
  156. package/lib/typescript/i18n/en.json +2 -0
  157. package/lib/typescript/i18n/es.json +2 -0
  158. package/lib/typescript/i18n/fr.json +2 -0
  159. package/lib/typescript/i18n/he.json +2 -0
  160. package/lib/typescript/i18n/hi.json +2 -0
  161. package/lib/typescript/i18n/it.json +2 -0
  162. package/lib/typescript/i18n/ja.json +2 -0
  163. package/lib/typescript/i18n/ko.json +2 -0
  164. package/lib/typescript/i18n/nl.json +2 -0
  165. package/lib/typescript/i18n/pt-br.json +2 -0
  166. package/lib/typescript/i18n/ru.json +2 -0
  167. package/lib/typescript/i18n/tr.json +2 -0
  168. package/lib/typescript/utils/i18n/Streami18n.d.ts +2 -0
  169. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  170. package/lib/typescript/utils/utils.d.ts.map +1 -1
  171. package/package.json +2 -2
  172. package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx +50 -0
  173. package/src/components/AITypingIndicatorView/hooks/useAIState.ts +68 -0
  174. package/src/components/AITypingIndicatorView/index.ts +2 -0
  175. package/src/components/Channel/Channel.tsx +26 -2
  176. package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +2 -0
  177. package/src/components/Channel/hooks/useCreateMessagesContext.ts +2 -0
  178. package/src/components/Message/Message.tsx +4 -1
  179. package/src/components/Message/MessageSimple/MessageContent.tsx +14 -2
  180. package/src/components/Message/MessageSimple/MessageFooter.tsx +6 -4
  181. package/src/components/Message/MessageSimple/MessageSimple.tsx +3 -1
  182. package/src/components/Message/MessageSimple/StreamingMessageView.tsx +34 -0
  183. package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts +9 -1
  184. package/src/components/Message/MessageSimple/utils/renderText.tsx +207 -3
  185. package/src/components/Message/hooks/useStreamingMessage.ts +54 -0
  186. package/src/components/MessageInput/MessageInput.tsx +19 -3
  187. package/src/components/MessageInput/StopMessageStreamingButton.tsx +34 -0
  188. package/src/components/MessageOverlay/MessageOverlay.tsx +10 -1
  189. package/src/components/index.ts +5 -0
  190. package/src/contexts/messageInputContext/MessageInputContext.tsx +5 -1
  191. package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +2 -0
  192. package/src/contexts/messagesContext/MessagesContext.tsx +6 -1
  193. package/src/contexts/themeContext/utils/theme.ts +14 -1
  194. package/src/i18n/en.json +2 -0
  195. package/src/i18n/es.json +2 -0
  196. package/src/i18n/fr.json +2 -0
  197. package/src/i18n/he.json +2 -0
  198. package/src/i18n/hi.json +2 -0
  199. package/src/i18n/it.json +2 -0
  200. package/src/i18n/ja.json +2 -0
  201. package/src/i18n/ko.json +2 -0
  202. package/src/i18n/nl.json +2 -0
  203. package/src/i18n/pt-br.json +2 -0
  204. package/src/i18n/ru.json +2 -0
  205. package/src/i18n/tr.json +2 -0
  206. package/src/utils/utils.ts +1 -1
  207. package/src/version.json +1 -1
@@ -100,6 +100,7 @@ export type MessageContentPropsWithContext<
100
100
  | 'myMessageTheme'
101
101
  | 'onPressInMessage'
102
102
  | 'Reply'
103
+ | 'StreamingMessageView'
103
104
  > &
104
105
  Pick<TranslationContextValue, 't'> & {
105
106
  setMessageContentWidth: React.Dispatch<React.SetStateAction<number>>;
@@ -142,6 +143,7 @@ const MessageContentWithContext = <
142
143
  Reply,
143
144
  setMessageContentWidth,
144
145
  showMessageStatus,
146
+ StreamingMessageView,
145
147
  threadList,
146
148
  } = props;
147
149
  const { client } = useChatContext();
@@ -393,9 +395,16 @@ const MessageContentWithContext = <
393
395
  />
394
396
  ) : null;
395
397
  }
398
+ case 'ai_text':
399
+ return message.ai_generated ? (
400
+ <StreamingMessageView
401
+ key={`ai_message_text_container_${messageContentOrderIndex}`}
402
+ />
403
+ ) : null;
396
404
  case 'text':
397
405
  default:
398
- return otherAttachments.length && otherAttachments[0].actions ? null : (
406
+ return (otherAttachments.length && otherAttachments[0].actions) ||
407
+ message.ai_generated ? null : (
399
408
  <MessageTextContainer<StreamChatGenerics>
400
409
  key={`message_text_container_${messageContentOrderIndex}`}
401
410
  />
@@ -484,7 +493,8 @@ const areEqual = <StreamChatGenerics extends DefaultStreamChatGenerics = Default
484
493
  prevMessage.type === nextMessage.type &&
485
494
  prevMessage.text === nextMessage.text &&
486
495
  prevMessage.pinned === nextMessage.pinned &&
487
- prevMessage.i18n === nextMessage.i18n;
496
+ prevMessage.i18n === nextMessage.i18n &&
497
+ prevMessage.ai_generated === nextMessage.ai_generated;
488
498
  if (!messageEqual) return false;
489
499
 
490
500
  const isPrevQuotedMessageTypeDeleted = prevMessage.quoted_message?.type === 'deleted';
@@ -597,6 +607,7 @@ export const MessageContent = <
597
607
  MessageStatus,
598
608
  myMessageTheme,
599
609
  Reply,
610
+ StreamingMessageView,
600
611
  } = useMessagesContext<StreamChatGenerics>();
601
612
  const { t } = useTranslationContext();
602
613
 
@@ -635,6 +646,7 @@ export const MessageContent = <
635
646
  preventPress,
636
647
  Reply,
637
648
  showMessageStatus,
649
+ StreamingMessageView,
638
650
  t,
639
651
  threadList,
640
652
  }}
@@ -129,6 +129,8 @@ const MessageFooterWithContext = <
129
129
  return null;
130
130
  }
131
131
 
132
+ const isEdited = isEditedMessage(message);
133
+
132
134
  return (
133
135
  <>
134
136
  <View style={[styles.container, metaContainer]} testID='message-status-time'>
@@ -141,7 +143,7 @@ const MessageFooterWithContext = <
141
143
  {showMessageStatus && <MessageStatus />}
142
144
  <MessageTimestamp formattedDate={formattedDate} timestamp={date} />
143
145
 
144
- {isEditedMessage(message) && !isEditedMessageOpen && (
146
+ {isEdited && !isEditedMessageOpen ? (
145
147
  <>
146
148
  <Text
147
149
  style={[
@@ -159,11 +161,11 @@ const MessageFooterWithContext = <
159
161
  {t<string>('Edited')}
160
162
  </Text>
161
163
  </>
162
- )}
164
+ ) : null}
163
165
  </View>
164
- {isEditedMessageOpen && (
166
+ {isEdited && isEditedMessageOpen ? (
165
167
  <MessageEditedTimestamp message={message} MessageTimestamp={MessageTimestamp} />
166
- )}
168
+ ) : null}
167
169
  </>
168
170
  );
169
171
  };
@@ -154,7 +154,9 @@ const areEqual = <StreamChatGenerics extends DefaultStreamChatGenerics = Default
154
154
  prevMessage.status === nextMessage.status &&
155
155
  prevMessage.type === nextMessage.type &&
156
156
  prevMessage.text === nextMessage.text &&
157
- prevMessage.i18n === nextMessage.i18n;
157
+ prevMessage.i18n === nextMessage.i18n &&
158
+ prevMessage.pinned === nextMessage.pinned &&
159
+ prevMessage.ai_generated === nextMessage.ai_generated;
158
160
  if (!messageEqual) return false;
159
161
 
160
162
  const isPrevQuotedMessageTypeDeleted = prevMessage.quoted_message?.type === 'deleted';
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+
3
+ import { MessageTextContainer, MessageTextContainerProps } from './MessageTextContainer';
4
+
5
+ import { useMessageContext } from '../../../contexts';
6
+ import type { DefaultStreamChatGenerics } from '../../../types/types';
7
+ import { useStreamingMessage } from '../hooks/useStreamingMessage';
8
+
9
+ export type StreamingMessageViewProps<
10
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
11
+ > = Pick<MessageTextContainerProps<StreamChatGenerics>, 'message'> & {
12
+ letterInterval?: number;
13
+ renderingLetterCount?: number;
14
+ };
15
+
16
+ export const StreamingMessageView = <
17
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
18
+ >(
19
+ props: StreamingMessageViewProps<StreamChatGenerics>,
20
+ ) => {
21
+ const { letterInterval, message: messageFromProps, renderingLetterCount } = props;
22
+ const { message: messageFromContext } = useMessageContext<StreamChatGenerics>();
23
+ const message = messageFromProps || messageFromContext;
24
+ const { text = '' } = message;
25
+ const { streamedMessageText } = useStreamingMessage({
26
+ letterInterval,
27
+ renderingLetterCount,
28
+ text,
29
+ });
30
+
31
+ return <MessageTextContainer message={{ ...message, text: streamedMessageText }} />;
32
+ };
33
+
34
+ StreamingMessageView.displayName = 'StreamingMessageView{messageSimple{content}}';
@@ -33,7 +33,11 @@ export const generateMarkdownText = (text?: string) => {
33
33
  resultText = resultText.replace(mentionsRegex, `@${displayLink}`);
34
34
  }
35
35
 
36
- resultText = resultText.replace(/[<"'>]/g, '\\$&');
36
+ // Escape the " and ' characters, except in code blocks where we deem this allowed.
37
+ resultText = resultText.replace(/(```[\s\S]*?```|`.*?`)|[<"'>]/g, (match, code) => {
38
+ if (code) return code;
39
+ return `\\${match}`;
40
+ });
37
41
 
38
42
  // Remove whitespaces that come directly after newlines except in code blocks where we deem this allowed.
39
43
  resultText = resultText.replace(/(```[\s\S]*?```|`.*?`)|\n[ ]{2,}/g, (_, code) => {
@@ -41,5 +45,9 @@ export const generateMarkdownText = (text?: string) => {
41
45
  return '\n';
42
46
  });
43
47
 
48
+ // Always replace \n``` with \n\n``` to force the markdown state machine to treat it as a separate block. Otherwise, code blocks inside of list
49
+ // items for example were broken. We clean up the code block closing state within the rendering itself.
50
+ resultText = resultText.replace(/\n```/g, '\n\n```');
51
+
44
52
  return resultText;
45
53
  };
@@ -1,8 +1,18 @@
1
- import React, { PropsWithChildren } from 'react';
2
- import { GestureResponderEvent, Linking, Text, TextProps, View, ViewProps } from 'react-native';
3
-
1
+ import React, { PropsWithChildren, ReactNode, useCallback, useMemo } from 'react';
2
+ import {
3
+ GestureResponderEvent,
4
+ Linking,
5
+ Platform,
6
+ Text,
7
+ TextProps,
8
+ View,
9
+ ViewProps,
10
+ } from 'react-native';
11
+
12
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
13
  // @ts-expect-error
5
14
  import Markdown from 'react-native-markdown-package';
15
+ import Animated, { clamp, scrollTo, useAnimatedRef, useSharedValue } from 'react-native-reanimated';
6
16
 
7
17
  import {
8
18
  DefaultRules,
@@ -26,7 +36,64 @@ import type { DefaultStreamChatGenerics } from '../../../../types/types';
26
36
  import { escapeRegExp } from '../../../../utils/utils';
27
37
  import type { MessageType } from '../../../MessageList/hooks/useMessageList';
28
38
 
39
+ export const MarkdownReactiveScrollView = ({ children }: { children: ReactNode }) => {
40
+ const scrollViewRef = useAnimatedRef<Animated.ScrollView>();
41
+ const contentWidth = useSharedValue(0);
42
+ const visibleContentWidth = useSharedValue(0);
43
+ const offsetBeforeScroll = useSharedValue(0);
44
+
45
+ const panGesture = Gesture.Pan()
46
+ .activeOffsetX([-5, 5])
47
+ .onUpdate((event) => {
48
+ const { translationX } = event;
49
+
50
+ scrollTo(scrollViewRef, offsetBeforeScroll.value - translationX, 0, false);
51
+ })
52
+ .onEnd((event) => {
53
+ const { translationX } = event;
54
+
55
+ const velocityEffect = event.velocityX * 0.3;
56
+
57
+ const finalPosition = clamp(
58
+ offsetBeforeScroll.value - translationX - velocityEffect,
59
+ 0,
60
+ contentWidth.value - visibleContentWidth.value,
61
+ );
62
+
63
+ offsetBeforeScroll.value = finalPosition;
64
+
65
+ scrollTo(scrollViewRef, finalPosition, 0, true);
66
+ });
67
+
68
+ return (
69
+ <View style={{ width: '100%' }}>
70
+ <GestureDetector gesture={panGesture}>
71
+ <Animated.ScrollView
72
+ contentContainerStyle={{ flexGrow: 1 }}
73
+ horizontal
74
+ nestedScrollEnabled={true}
75
+ onContentSizeChange={(width) => {
76
+ contentWidth.value = width;
77
+ }}
78
+ onLayout={(e) => {
79
+ visibleContentWidth.value = e.nativeEvent.layout.width;
80
+ }}
81
+ ref={scrollViewRef}
82
+ scrollEnabled={false}
83
+ >
84
+ {children}
85
+ </Animated.ScrollView>
86
+ </GestureDetector>
87
+ </View>
88
+ );
89
+ };
90
+
29
91
  const defaultMarkdownStyles: MarkdownStyle = {
92
+ codeBlock: {
93
+ fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace',
94
+ fontWeight: '500',
95
+ marginVertical: 8,
96
+ },
30
97
  inlineCode: {
31
98
  fontSize: 13,
32
99
  padding: 3,
@@ -60,6 +127,26 @@ const defaultMarkdownStyles: MarkdownStyle = {
60
127
  marginBottom: 8,
61
128
  marginTop: 8,
62
129
  },
130
+ table: {
131
+ borderRadius: 3,
132
+ borderWidth: 1,
133
+ flex: 1,
134
+ flexDirection: 'row',
135
+ },
136
+ tableHeader: {
137
+ flexDirection: 'row',
138
+ justifyContent: 'space-around',
139
+ },
140
+ tableHeaderCell: {
141
+ fontWeight: '500',
142
+ },
143
+ tableRow: {
144
+ alignItems: 'center',
145
+ justifyContent: 'space-around',
146
+ },
147
+ tableRowCell: {
148
+ flex: 1,
149
+ },
63
150
  };
64
151
 
65
152
  const mentionsParseFunction: ParseFunction = (capture, parse, state) => ({
@@ -113,6 +200,13 @@ export const renderText = <
113
200
  color: colors.accent_blue,
114
201
  ...markdownStyles?.autolink,
115
202
  },
203
+ codeBlock: {
204
+ ...defaultMarkdownStyles.codeBlock,
205
+ backgroundColor: colors.code_block,
206
+ color: colors.black,
207
+ padding: 8,
208
+ ...markdownStyles?.codeBlock,
209
+ },
116
210
  inlineCode: {
117
211
  ...defaultMarkdownStyles.inlineCode,
118
212
  backgroundColor: colors.white_smoke,
@@ -125,6 +219,35 @@ export const renderText = <
125
219
  color: colors.accent_blue,
126
220
  ...markdownStyles?.mentions,
127
221
  },
222
+ table: {
223
+ ...defaultMarkdownStyles.table,
224
+ borderColor: colors.grey_dark,
225
+ marginVertical: 8,
226
+ ...markdownStyles?.table,
227
+ },
228
+ tableHeader: {
229
+ ...defaultMarkdownStyles.tableHeader,
230
+ backgroundColor: colors.grey,
231
+ ...markdownStyles?.tableHeader,
232
+ },
233
+ tableHeaderCell: {
234
+ ...defaultMarkdownStyles.tableHeaderCell,
235
+ padding: 5,
236
+ ...markdownStyles?.tableHeaderCell,
237
+ },
238
+ tableRow: {
239
+ ...defaultMarkdownStyles.tableRow,
240
+ ...markdownStyles?.tableRow,
241
+ },
242
+ tableRowCell: {
243
+ ...defaultMarkdownStyles.tableRowCell,
244
+ borderColor: colors.grey_dark,
245
+ padding: 5,
246
+ ...markdownStyles?.tableRowCell,
247
+ },
248
+ tableRowLast: {
249
+ ...markdownStyles?.tableRowLast,
250
+ },
128
251
  text: {
129
252
  ...defaultMarkdownStyles.text,
130
253
  color: colors.black,
@@ -263,6 +386,18 @@ export const renderText = <
263
386
  />
264
387
  );
265
388
 
389
+ const codeBlockReact: ReactNodeOutput = (node, _, state) => (
390
+ <MarkdownReactiveScrollView key={state.key}>
391
+ <Text style={styles.codeBlock}>{node?.content?.trim()}</Text>
392
+ </MarkdownReactiveScrollView>
393
+ );
394
+
395
+ const tableReact: ReactNodeOutput = (node, output, state) => (
396
+ <MarkdownReactiveScrollView key={state.key}>
397
+ <MarkdownTable node={node} output={output} state={state} styles={styles} />
398
+ </MarkdownReactiveScrollView>
399
+ );
400
+
266
401
  const customRules = {
267
402
  // do not render images, we will scrape them out of the message and show on attachment card component
268
403
  image: { match: () => null },
@@ -283,6 +418,8 @@ export const renderText = <
283
418
  },
284
419
  }
285
420
  : {}),
421
+ codeBlock: { react: codeBlockReact },
422
+ table: { react: tableReact },
286
423
  };
287
424
 
288
425
  return (
@@ -373,3 +510,70 @@ const ListRow = ({ children, style }: PropsWithChildren<ViewProps>) => (
373
510
  const ListItem = ({ children, style }: PropsWithChildren<TextProps>) => (
374
511
  <Text style={style}>{children}</Text>
375
512
  );
513
+
514
+ export type MarkdownTableProps = {
515
+ node: SingleASTNode;
516
+ output: ReactOutput;
517
+ state: State;
518
+ styles: Partial<MarkdownStyle>;
519
+ };
520
+
521
+ const transpose = (matrix: SingleASTNode[][]) =>
522
+ matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
523
+
524
+ const MarkdownTable = ({ node, output, state, styles }: MarkdownTableProps) => {
525
+ const content = useMemo(() => {
526
+ const nodeContent = [node?.header, ...node?.cells];
527
+ return transpose(nodeContent);
528
+ }, [node?.cells, node?.header]);
529
+ const columns = content?.map((column, idx) => (
530
+ <MarkdownTableColumn
531
+ items={column}
532
+ key={`column-${idx}`}
533
+ output={output}
534
+ state={state}
535
+ styles={styles}
536
+ />
537
+ ));
538
+
539
+ return (
540
+ <View key={state.key} style={styles.table}>
541
+ {columns}
542
+ </View>
543
+ );
544
+ };
545
+
546
+ export type MarkdownTableRowProps = {
547
+ items: SingleASTNode[];
548
+ output: ReactOutput;
549
+ state: State;
550
+ styles: Partial<MarkdownStyle>;
551
+ };
552
+
553
+ const MarkdownTableColumn = ({ items, output, state, styles }: MarkdownTableRowProps) => {
554
+ const [headerCellContent, ...columnCellContents] = items;
555
+
556
+ const ColumnCell = useCallback(
557
+ ({ content }: { content: SingleASTNode }) =>
558
+ content ? (
559
+ <View style={styles.tableRow}>
560
+ <View style={styles.tableRowCell}>{output(content, state)}</View>
561
+ </View>
562
+ ) : null,
563
+ [output, state, styles],
564
+ );
565
+
566
+ return (
567
+ <View style={{ flex: 1, flexDirection: 'column' }}>
568
+ {headerCellContent ? (
569
+ <View key={-1} style={styles.tableHeader}>
570
+ <Text style={styles.tableHeaderCell}>{output(headerCellContent, state)}</Text>
571
+ </View>
572
+ ) : null}
573
+ {columnCellContents &&
574
+ columnCellContents.map((content, idx) => (
575
+ <ColumnCell content={content} key={`cell-${idx}`} />
576
+ ))}
577
+ </View>
578
+ );
579
+ };
@@ -0,0 +1,54 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ import type { DefaultStreamChatGenerics } from '../../../types/types';
4
+ import { StreamingMessageViewProps } from '../MessageSimple/StreamingMessageView';
5
+
6
+ export type UseStreamingMessageProps<
7
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
8
+ > = Pick<
9
+ StreamingMessageViewProps<StreamChatGenerics>,
10
+ 'letterInterval' | 'renderingLetterCount'
11
+ > & { text: string };
12
+
13
+ const DEFAULT_LETTER_INTERVAL = 0;
14
+ const DEFAULT_RENDERING_LETTER_COUNT = 2;
15
+
16
+ /**
17
+ * A hook that returns text in a streamed, typewriter fashion. The speed of streaming is
18
+ * configurable.
19
+ * @param {number} [letterInterval=0] - The timeout between each typing animation in milliseconds.
20
+ * @param {number} [renderingLetterCount=2] - The number of letters to be rendered each time we update.
21
+ * @param {string} text - The text that we want to render in a typewriter fashion.
22
+ * @returns {{ streamedMessageText: string }} - A substring of the text property, up until we've finished rendering the typewriter animation.
23
+ */
24
+ export const useStreamingMessage = <
25
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
26
+ >({
27
+ letterInterval = DEFAULT_LETTER_INTERVAL,
28
+ renderingLetterCount = DEFAULT_RENDERING_LETTER_COUNT,
29
+ text,
30
+ }: UseStreamingMessageProps<StreamChatGenerics>): { streamedMessageText: string } => {
31
+ const [streamedMessageText, setStreamedMessageText] = useState<string>(text);
32
+ const textCursor = useRef<number>(text.length);
33
+
34
+ useEffect(() => {
35
+ const textLength = text.length;
36
+ const interval = setInterval(() => {
37
+ if (!text || textCursor.current >= textLength) {
38
+ clearInterval(interval);
39
+ }
40
+ const newCursorValue = textCursor.current + renderingLetterCount;
41
+ const newText = text.substring(0, newCursorValue);
42
+ textCursor.current += newText.length - textCursor.current;
43
+ const codeBlockCounts = (newText.match(/```/g) || []).length;
44
+ const shouldOptimisticallyCloseCodeBlock = codeBlockCounts > 0 && codeBlockCounts % 2 > 0;
45
+ setStreamedMessageText(shouldOptimisticallyCloseCodeBlock ? newText + '```' : newText);
46
+ }, letterInterval);
47
+
48
+ return () => {
49
+ clearInterval(interval);
50
+ };
51
+ }, [letterInterval, renderingLetterCount, text]);
52
+
53
+ return { streamedMessageText };
54
+ };
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  Modal,
4
4
  NativeSyntheticEvent,
@@ -58,6 +58,7 @@ import {
58
58
 
59
59
  import { isImageMediaLibraryAvailable, triggerHaptic } from '../../native';
60
60
  import type { Asset, DefaultStreamChatGenerics } from '../../types/types';
61
+ import { AIStates, useAIState } from '../AITypingIndicatorView';
61
62
  import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput';
62
63
  import { CreatePoll } from '../Poll/CreatePollContent';
63
64
 
@@ -159,6 +160,7 @@ type MessageInputPropsWithContext<
159
160
  | 'showPollCreationDialog'
160
161
  | 'sendMessage'
161
162
  | 'CreatePollContent'
163
+ | 'StopMessageStreamingButton'
162
164
  > &
163
165
  Pick<MessagesContextValue<StreamChatGenerics>, 'Reply'> &
164
166
  Pick<
@@ -228,6 +230,7 @@ const MessageInputWithContext = <
228
230
  showPollCreationDialog,
229
231
  ShowThreadMessageInChannelButton,
230
232
  StartAudioRecordingButton,
233
+ StopMessageStreamingButton,
231
234
  suggestions,
232
235
  text,
233
236
  thread,
@@ -728,6 +731,13 @@ const MessageInputWithContext = <
728
731
  })),
729
732
  };
730
733
 
734
+ const { channel } = useChannelContext<StreamChatGenerics>();
735
+ const { aiState } = useAIState(channel);
736
+
737
+ const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]);
738
+ const shouldDisplayStopAIGeneration =
739
+ [AIStates.Thinking, AIStates.Generating].includes(aiState) && !!StopMessageStreamingButton;
740
+
731
741
  return (
732
742
  <>
733
743
  <View
@@ -833,7 +843,10 @@ const MessageInputWithContext = <
833
843
  </>
834
844
  )}
835
845
 
836
- {isSendingButtonVisible() &&
846
+ {shouldDisplayStopAIGeneration ? (
847
+ <StopMessageStreamingButton onPress={stopGenerating} />
848
+ ) : (
849
+ isSendingButtonVisible() &&
837
850
  (cooldownRemainingSeconds ? (
838
851
  <CooldownTimer seconds={cooldownRemainingSeconds} />
839
852
  ) : (
@@ -842,7 +855,8 @@ const MessageInputWithContext = <
842
855
  disabled={sending.current || !isValidMessage() || (giphyActive && !isOnline)}
843
856
  />
844
857
  </View>
845
- ))}
858
+ ))
859
+ )}
846
860
  {audioRecordingEnabled && !micLocked && (
847
861
  <GestureDetector gesture={panGestureMic}>
848
862
  <Animated.View
@@ -1144,6 +1158,7 @@ export const MessageInput = <
1144
1158
  showPollCreationDialog,
1145
1159
  ShowThreadMessageInChannelButton,
1146
1160
  StartAudioRecordingButton,
1161
+ StopMessageStreamingButton,
1147
1162
  text,
1148
1163
  uploadNewFile,
1149
1164
  uploadNewImage,
@@ -1233,6 +1248,7 @@ export const MessageInput = <
1233
1248
  showPollCreationDialog,
1234
1249
  ShowThreadMessageInChannelButton,
1235
1250
  StartAudioRecordingButton,
1251
+ StopMessageStreamingButton,
1236
1252
  suggestions,
1237
1253
  t,
1238
1254
  text,
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Pressable } from 'react-native';
3
+
4
+ import { useTheme } from '../../contexts/themeContext/ThemeContext';
5
+ import { CircleStop } from '../../icons';
6
+
7
+ export type StopMessageStreamingButtonProps = {
8
+ /** Function that opens attachment options bottom sheet */
9
+ onPress?: () => void;
10
+ };
11
+
12
+ export const StopMessageStreamingButton = (props: StopMessageStreamingButtonProps) => {
13
+ const { onPress } = props;
14
+
15
+ const {
16
+ theme: {
17
+ colors: { accent_blue },
18
+ messageInput: { stopMessageStreamingButton, stopMessageStreamingIcon },
19
+ },
20
+ } = useTheme();
21
+
22
+ return (
23
+ <Pressable
24
+ hitSlop={{ bottom: 15, left: 15, right: 15, top: 15 }}
25
+ onPress={onPress}
26
+ style={[stopMessageStreamingButton]}
27
+ testID='more-options-button'
28
+ >
29
+ <CircleStop fill={accent_blue} size={32} {...stopMessageStreamingIcon} />
30
+ </Pressable>
31
+ );
32
+ };
33
+
34
+ StopMessageStreamingButton.displayName = 'StopMessageStreamingButton{messageInput}';
@@ -45,6 +45,7 @@ import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContex
45
45
  import { useViewport } from '../../hooks/useViewport';
46
46
  import type { DefaultStreamChatGenerics } from '../../types/types';
47
47
  import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer';
48
+ import { StreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
48
49
  import { OverlayReactions as DefaultOverlayReactions } from '../MessageOverlay/OverlayReactions';
49
50
  import type { ReplyProps } from '../Reply/Reply';
50
51
 
@@ -451,9 +452,17 @@ const MessageOverlayWithContext = <
451
452
  </OwnCapabilitiesProvider>
452
453
  ) : null;
453
454
  }
455
+ case 'ai_text':
456
+ return (
457
+ <StreamingMessageView
458
+ key={`ai_message_text_container_${messageContentOrderIndex}`}
459
+ message={message}
460
+ />
461
+ );
454
462
  case 'text':
455
463
  default:
456
- return otherAttachments?.length && otherAttachments[0].actions ? null : (
464
+ return (otherAttachments?.length && otherAttachments[0].actions) ||
465
+ message.ai_generated ? null : (
457
466
  <MessageTextContainer<StreamChatGenerics>
458
467
  key={`message_text_container_${messageContentOrderIndex}`}
459
468
  message={message}
@@ -97,6 +97,7 @@ export * from './KeyboardCompatibleView/KeyboardCompatibleView';
97
97
  export * from './Message/hooks/useCreateMessageContext';
98
98
  export * from './Message/hooks/useMessageActions';
99
99
  export * from './Message/hooks/useMessageActionHandlers';
100
+ export * from './Message/hooks/useStreamingMessage';
100
101
  export * from './Message/Message';
101
102
  export * from './Message/MessageSimple/MessageAvatar';
102
103
  export * from './Message/MessageSimple/MessageBounce';
@@ -132,6 +133,7 @@ export * from './MessageInput/InputButtons';
132
133
  export * from './MessageInput/MessageInput';
133
134
  export * from './MessageInput/MoreOptionsButton';
134
135
  export * from './MessageInput/SendButton';
136
+ export * from './MessageInput/StopMessageStreamingButton';
135
137
  export * from './MessageInput/ShowThreadMessageInChannelButton';
136
138
  export * from './MessageInput/UploadProgressIndicator';
137
139
 
@@ -172,3 +174,6 @@ export * from './Spinner/Spinner';
172
174
  export * from './Thread/Thread';
173
175
  export * from './Thread/components/ThreadFooterComponent';
174
176
  export * from './ThreadList/ThreadList';
177
+
178
+ export * from './Message/MessageSimple/StreamingMessageView';
179
+ export * from './AITypingIndicatorView';
@@ -29,7 +29,7 @@ import { useMessageDetailsForState } from './hooks/useMessageDetailsForState';
29
29
 
30
30
  import { isUploadAllowed, MAX_FILE_SIZE_TO_UPLOAD, prettifyFileSize } from './utils/utils';
31
31
 
32
- import { PollContentProps } from '../../components';
32
+ import { PollContentProps, StopMessageStreamingButtonProps } from '../../components';
33
33
  import { AudioAttachmentProps } from '../../components/Attachment/AudioAttachment';
34
34
  import { parseLinksFromText } from '../../components/Message/MessageSimple/utils/parseLinks';
35
35
  import type { AttachButtonProps } from '../../components/MessageInput/AttachButton';
@@ -385,6 +385,7 @@ export type InputMessageInputContextValue<
385
385
  * Defaults to and accepts same props as: [AudioRecordingButton](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx)
386
386
  */
387
387
  StartAudioRecordingButton: React.ComponentType<AudioRecordingButtonProps<StreamChatGenerics>>;
388
+ StopMessageStreamingButton: React.ComponentType<StopMessageStreamingButtonProps>;
388
389
  /**
389
390
  * Custom UI component to render upload progress indicator on attachment preview.
390
391
  *
@@ -586,7 +587,9 @@ export const MessageInputProvider = <
586
587
  editing,
587
588
  initialValue,
588
589
  openPollCreationDialog: openPollCreationDialogFromContext,
590
+ StopMessageStreamingButton,
589
591
  } = value;
592
+
590
593
  const {
591
594
  fileUploads,
592
595
  imageUploads,
@@ -1481,6 +1484,7 @@ export const MessageInputProvider = <
1481
1484
  openPollCreationDialog,
1482
1485
  sendMessage, // overriding the originally passed in sendMessage
1483
1486
  showPollCreationDialog,
1487
+ StopMessageStreamingButton,
1484
1488
  });
1485
1489
  return (
1486
1490
  <MessageInputContext.Provider