diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index 1b81592ba0..6e25a65a7e 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -7310,10 +7310,10 @@ stream-buffers@2.2.x, stream-buffers@~2.2.0: version "0.0.0" uid "" -stream-chat-react-native-core@5.42.2: - version "5.42.2" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.42.2.tgz#edb92b490d4beb894ba549c7deefc770f501360c" - integrity sha512-eA2fK3eYdhr2RSbB22s1ha29y4EPbj9yPAskWxGo1aJK+KXwXDlAYqGtJa/8saRSSU4DdV89yG5ihvTFgP+Huw== +stream-chat-react-native-core@5.43.1: + version "5.43.1" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.1.tgz#ec0d5a06e329c8991c46cff5bd0211bc94d2b26f" + integrity sha512-qj/WfjFeCCP2wcp1YZGFJgRYGdSWXd0maG3hn3oURgFR6p/BmO6lDL2g3jnLq0SEkD8x+KZeNBS9cs1gW5Gblw== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -7326,27 +7326,12 @@ stream-chat-react-native-core@5.42.2: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.44.0" + stream-chat "8.45.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" uid "" -stream-chat@8.44.0: - version "8.44.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.44.0.tgz#e48446cf91db786e80a2e7358ac4f1601e5e97f0" - integrity sha512-7HNtimD8sT/51rsFibGcD6uBgKj+vlKyYCZMDzjYQEaEsrLqyAg48dOyNM4L2FTF5aXgo9SlxZr21SPleeA2BA== - dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" - axios "^1.6.0" - base64-js "^1.5.1" - form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" - stream-chat@8.45.1: version "8.45.1" resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.45.1.tgz#001f452520602ebffe45f1634b5f5dd1ff036ec5" diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index fc24f34dc7..742904914a 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -1556,7 +1556,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f + glog: 69ef571f3de08433d766d614c73a9838a06bf7eb GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 diff --git a/examples/SampleApp/src/hooks/useStreamChatTheme.ts b/examples/SampleApp/src/hooks/useStreamChatTheme.ts index e241ccd2b5..ff12a49518 100644 --- a/examples/SampleApp/src/hooks/useStreamChatTheme.ts +++ b/examples/SampleApp/src/hooks/useStreamChatTheme.ts @@ -17,6 +17,7 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial => ({ border: '#141924', button_background: '#FFFFFF', button_text: '#005FFF', + code_block: '#222222', grey: '#7A7A7A', grey_gainsboro: '#2D2F2F', grey_whisper: '#1C1E22', diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 2daaea56d9..db3d768ca3 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -12,6 +12,7 @@ import { useChatContext, useTheme, useTypingString, + AITypingIndicatorView, } from 'stream-chat-react-native'; import { Platform, StyleSheet, View } from 'react-native'; import type { StackNavigationProp } from '@react-navigation/stack'; @@ -168,6 +169,7 @@ export const ChannelScreen: React.FC = ({ }); }} /> + diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index fe345d8a88..07526b0fd8 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -6837,10 +6837,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@5.42.2: - version "5.42.2" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.42.2.tgz#edb92b490d4beb894ba549c7deefc770f501360c" - integrity sha512-eA2fK3eYdhr2RSbB22s1ha29y4EPbj9yPAskWxGo1aJK+KXwXDlAYqGtJa/8saRSSU4DdV89yG5ihvTFgP+Huw== +stream-chat-react-native-core@5.43.2: + version "5.43.2" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.2.tgz#b16add60d231509f864d7301ae281c4b4681ff7a" + integrity sha512-pdSaqw1aPHtxH0md7nnw/TLWPIqsb5TKFzGoWYd/cKRAw4pVDvxolSlha8vYSCwz7MmlarfsJPB0TlS9WcGsDQ== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -6853,7 +6853,7 @@ stream-chat-react-native-core@5.42.2: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.44.0" + stream-chat "8.45.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" @@ -6863,10 +6863,10 @@ stream-chat-react-native-core@5.42.2: version "0.0.0" uid "" -stream-chat@8.44.0: - version "8.44.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.44.0.tgz#e48446cf91db786e80a2e7358ac4f1601e5e97f0" - integrity sha512-7HNtimD8sT/51rsFibGcD6uBgKj+vlKyYCZMDzjYQEaEsrLqyAg48dOyNM4L2FTF5aXgo9SlxZr21SPleeA2BA== +stream-chat@8.45.1: + version "8.45.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.45.1.tgz#001f452520602ebffe45f1634b5f5dd1ff036ec5" + integrity sha512-7OMpL2RHUd+PXSWzhTUAIjjXlI9Oqc4HhUBRfc5i6dK+Ug9S5ertb7RvyzGL5N4ITpq/6ZUAwXtTqRaN9+UUkw== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" @@ -6878,10 +6878,10 @@ stream-chat@8.44.0: jsonwebtoken "~9.0.0" ws "^7.5.10" -stream-chat@8.45.1: - version "8.45.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.45.1.tgz#001f452520602ebffe45f1634b5f5dd1ff036ec5" - integrity sha512-7OMpL2RHUd+PXSWzhTUAIjjXlI9Oqc4HhUBRfc5i6dK+Ug9S5ertb7RvyzGL5N4ITpq/6ZUAwXtTqRaN9+UUkw== +stream-chat@8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.46.0.tgz#416b325e05b144d0937a3527d1e622463113d605" + integrity sha512-HQVCRVldrfQFAvsBOHiHR0TKYf+wpsg/cAzRojeZY+buy1vG6eoqk09h6Fl4k2eG3zFLoA0G9W6o7o45jyFE1g== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" diff --git a/examples/TypeScriptMessaging/ios/Podfile.lock b/examples/TypeScriptMessaging/ios/Podfile.lock index bfb945d6c3..f6593edd98 100644 --- a/examples/TypeScriptMessaging/ios/Podfile.lock +++ b/examples/TypeScriptMessaging/ios/Podfile.lock @@ -1429,7 +1429,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 + DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 FBLazyVector: f64d1e2ea739b4d8f7e4740cde18089cd97fe864 FBReactNativeSpec: 9f2b8b243131565335437dba74923a8d3015e780 Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44 @@ -1441,7 +1441,7 @@ SPEC CHECKSUMS: Flipper-PeerTalk: 116d8f857dc6ef55c7a5a75ea3ceaafe878aadc9 FlipperKit: 37525a5d056ef9b93d1578e04bc3ea1de940094f fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 69ef571f3de08433d766d614c73a9838a06bf7eb + glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9cecf9953a681df7556b8cc9c74905de8f3293c0 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock index b3af9d70da..3dc027f561 100644 --- a/examples/TypeScriptMessaging/yarn.lock +++ b/examples/TypeScriptMessaging/yarn.lock @@ -6899,10 +6899,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@5.42.2: - version "5.42.2" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.42.2.tgz#edb92b490d4beb894ba549c7deefc770f501360c" - integrity sha512-eA2fK3eYdhr2RSbB22s1ha29y4EPbj9yPAskWxGo1aJK+KXwXDlAYqGtJa/8saRSSU4DdV89yG5ihvTFgP+Huw== +stream-chat-react-native-core@5.43.1: + version "5.43.1" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.1.tgz#ec0d5a06e329c8991c46cff5bd0211bc94d2b26f" + integrity sha512-qj/WfjFeCCP2wcp1YZGFJgRYGdSWXd0maG3hn3oURgFR6p/BmO6lDL2g3jnLq0SEkD8x+KZeNBS9cs1gW5Gblw== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -6915,7 +6915,7 @@ stream-chat-react-native-core@5.42.2: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.44.0" + stream-chat "8.45.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" @@ -6930,21 +6930,6 @@ stream-chat-react-native-devtools@^1.1.0: version "0.0.0" uid "" -stream-chat@8.44.0: - version "8.44.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.44.0.tgz#e48446cf91db786e80a2e7358ac4f1601e5e97f0" - integrity sha512-7HNtimD8sT/51rsFibGcD6uBgKj+vlKyYCZMDzjYQEaEsrLqyAg48dOyNM4L2FTF5aXgo9SlxZr21SPleeA2BA== - dependencies: - "@babel/runtime" "^7.16.3" - "@types/jsonwebtoken" "~9.0.0" - "@types/ws" "^7.4.0" - axios "^1.6.0" - base64-js "^1.5.1" - form-data "^4.0.0" - isomorphic-ws "^4.0.1" - jsonwebtoken "~9.0.0" - ws "^7.5.10" - stream-chat@8.45.1: version "8.45.1" resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.45.1.tgz#001f452520602ebffe45f1634b5f5dd1ff036ec5" diff --git a/package/expo-package/yarn.lock b/package/expo-package/yarn.lock index 38134d135d..52aaa4480c 100644 --- a/package/expo-package/yarn.lock +++ b/package/expo-package/yarn.lock @@ -2929,10 +2929,10 @@ stream-buffers@2.2.x: resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4" integrity sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg== -stream-chat-react-native-core@5.43.1: - version "5.43.1" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.1.tgz#ec0d5a06e329c8991c46cff5bd0211bc94d2b26f" - integrity sha512-qj/WfjFeCCP2wcp1YZGFJgRYGdSWXd0maG3hn3oURgFR6p/BmO6lDL2g3jnLq0SEkD8x+KZeNBS9cs1gW5Gblw== +stream-chat-react-native-core@5.43.2: + version "5.43.2" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.2.tgz#b16add60d231509f864d7301ae281c4b4681ff7a" + integrity sha512-pdSaqw1aPHtxH0md7nnw/TLWPIqsb5TKFzGoWYd/cKRAw4pVDvxolSlha8vYSCwz7MmlarfsJPB0TlS9WcGsDQ== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" diff --git a/package/native-package/yarn.lock b/package/native-package/yarn.lock index dc5d6bf9e1..32fdd89efd 100644 --- a/package/native-package/yarn.lock +++ b/package/native-package/yarn.lock @@ -4244,10 +4244,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@5.43.1: - version "5.43.1" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.1.tgz#ec0d5a06e329c8991c46cff5bd0211bc94d2b26f" - integrity sha512-qj/WfjFeCCP2wcp1YZGFJgRYGdSWXd0maG3hn3oURgFR6p/BmO6lDL2g3jnLq0SEkD8x+KZeNBS9cs1gW5Gblw== +stream-chat-react-native-core@5.43.2: + version "5.43.2" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.43.2.tgz#b16add60d231509f864d7301ae281c4b4681ff7a" + integrity sha512-pdSaqw1aPHtxH0md7nnw/TLWPIqsb5TKFzGoWYd/cKRAw4pVDvxolSlha8vYSCwz7MmlarfsJPB0TlS9WcGsDQ== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" diff --git a/package/package.json b/package/package.json index 0c49bae870..97d78e81d6 100644 --- a/package/package.json +++ b/package/package.json @@ -78,7 +78,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^1.3.0", - "stream-chat": "8.45.1" + "stream-chat": "8.46.0" }, "peerDependencies": { "react-native-quick-sqlite": ">=5.1.0", diff --git a/package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx b/package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx new file mode 100644 index 0000000000..1709b8fcad --- /dev/null +++ b/package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { StyleSheet, Text, View } from 'react-native'; + +import { Channel } from 'stream-chat'; + +import { AIStates, useAIState } from './hooks/useAIState'; + +import { useChannelContext, useTheme, useTranslationContext } from '../../contexts'; +import type { DefaultStreamChatGenerics } from '../../types/types'; + +export type AITypingIndicatorViewProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + channel?: Channel; +}; + +export const AITypingIndicatorView = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + channel: channelFromProps, +}: AITypingIndicatorViewProps) => { + const { t } = useTranslationContext(); + const { channel: channelFromContext } = useChannelContext(); + const channel = channelFromProps || channelFromContext; + const { aiState } = useAIState(channel); + const allowedStates = { + [AIStates.Thinking]: t('Thinking...'), + [AIStates.Generating]: t('Generating...'), + }; + + const { + theme: { + aiTypingIndicatorView: { container, text }, + colors: { black, grey_gainsboro }, + }, + } = useTheme(); + + return aiState in allowedStates ? ( + + {allowedStates[aiState]} + + ) : null; +}; + +AITypingIndicatorView.displayName = 'AITypingIndicatorView{messageSimple{content}}'; + +const styles = StyleSheet.create({ + container: { paddingHorizontal: 16, paddingVertical: 18 }, +}); diff --git a/package/src/components/AITypingIndicatorView/hooks/useAIState.ts b/package/src/components/AITypingIndicatorView/hooks/useAIState.ts new file mode 100644 index 0000000000..3936f5811d --- /dev/null +++ b/package/src/components/AITypingIndicatorView/hooks/useAIState.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; + +import { AIState, Channel, Event } from 'stream-chat'; + +import { useChatContext } from '../../../contexts'; +import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { useIsOnline } from '../../Chat/hooks/useIsOnline'; + +export const AIStates = { + Error: 'AI_STATE_ERROR', + ExternalSources: 'AI_STATE_EXTERNAL_SOURCES', + Generating: 'AI_STATE_GENERATING', + Idle: 'AI_STATE_IDLE', + Thinking: 'AI_STATE_THINKING', +}; + +/** + * A hook that returns the current state of the AI. + * @param {Channel} channel - The channel for which we want to know the AI state. + * @returns {{ aiState: AIState }} The current AI state for the given channel. + */ +export const useAIState = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + channel?: Channel, +): { aiState: AIState } => { + const { client } = useChatContext(); + const { isOnline } = useIsOnline(client); + + const [aiState, setAiState] = useState(AIStates.Idle); + + useEffect(() => { + if (!isOnline) { + setAiState(AIStates.Idle); + } + }, [isOnline]); + + useEffect(() => { + if (!channel) { + return; + } + + const indicatorChangedListener = channel.on( + 'ai_indicator.update', + (event: Event) => { + const { cid } = event; + const state = event.ai_state as AIState; + if (channel.cid === cid) { + setAiState(state); + } + }, + ); + + const indicatorClearedListener = channel.on('ai_indicator.clear', (event) => { + const { cid } = event; + if (channel.cid === cid) { + setAiState(AIStates.Idle); + } + }); + + return () => { + indicatorChangedListener.unsubscribe(); + indicatorClearedListener.unsubscribe(); + }; + }, [channel]); + + return { aiState }; +}; diff --git a/package/src/components/AITypingIndicatorView/index.ts b/package/src/components/AITypingIndicatorView/index.ts new file mode 100644 index 0000000000..a7c895128b --- /dev/null +++ b/package/src/components/AITypingIndicatorView/index.ts @@ -0,0 +1,2 @@ +export * from './AITypingIndicatorView'; +export * from './hooks/useAIState'; diff --git a/package/src/components/Attachment/AudioAttachment.tsx b/package/src/components/Attachment/AudioAttachment.tsx index eb4d7e9f16..9109159594 100644 --- a/package/src/components/Attachment/AudioAttachment.tsx +++ b/package/src/components/Attachment/AudioAttachment.tsx @@ -36,6 +36,7 @@ export type AudioAttachmentProps = { */ export const AudioAttachment = (props: AudioAttachmentProps) => { const [width, setWidth] = useState(0); + const [progressControlTextWidth, setProgressControlTextWidth] = useState(0); const [currentSpeed, setCurrentSpeed] = useState(1.0); const soundRef = React.useRef(null); const { @@ -228,9 +229,6 @@ export const AudioAttachment = (props: AudioAttachmentProps) => { return ( { - setWidth(nativeEvent.layout.width); - }} style={[ styles.container, { @@ -256,7 +254,12 @@ export const AudioAttachment = (props: AudioAttachmentProps) => { )} - + { + setWidth(nativeEvent.layout.width); + }} + style={[styles.leftContainer, leftContainer]} + > { styles.filenameText, { color: black, - width: - 16 - // 16 = horizontal padding - 40 - // 40 = file icon size - 24 - // 24 = close icon size - 24, // 24 = internal padding }, I18nManager.isRTL ? { writingDirection: 'rtl' } : { writingDirection: 'ltr' }, filenameText, @@ -290,7 +288,12 @@ export const AudioAttachment = (props: AudioAttachmentProps) => { uri={item.file.uri} /> )} - + { + setProgressControlTextWidth(nativeEvent.layout.width); + }} + style={[styles.progressDurationText, { color: grey_dark }, progressDurationText]} + > {progressDuration} {!hideProgressBar && ( @@ -316,7 +319,7 @@ export const AudioAttachment = (props: AudioAttachmentProps) => { onProgressDrag={handleProgressDrag} progress={item.progress as number} testID='progress-control' - width={width / 2} + width={width - progressControlTextWidth} /> )} @@ -365,35 +368,33 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: 'bold', paddingBottom: 12, - paddingLeft: 8, }, leftContainer: { justifyContent: 'space-around', + marginHorizontal: 16, + width: '60%', }, playPauseButton: { alignItems: 'center', alignSelf: 'center', borderRadius: 50, - display: 'flex', elevation: 4, justifyContent: 'center', - paddingVertical: 2, + padding: 2, shadowOffset: { height: 2, width: 0, }, shadowOpacity: 0.23, shadowRadius: 2.62, - width: 36, }, progressControlContainer: {}, progressDurationText: { fontSize: 12, - paddingLeft: 10, - paddingRight: 8, + marginRight: 4, }, rightContainer: { - marginLeft: 10, + marginLeft: 'auto', }, speedChangeButton: { alignItems: 'center', @@ -401,6 +402,7 @@ const styles = StyleSheet.create({ borderRadius: 50, elevation: 4, justifyContent: 'center', + paddingHorizontal: 10, paddingVertical: 5, shadowOffset: { height: 2, @@ -408,7 +410,6 @@ const styles = StyleSheet.create({ }, shadowOpacity: 0.23, shadowRadius: 2.62, - width: 36, }, speedChangeButtonText: { fontSize: 12, diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 8c92071512..dffd98eff9 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -135,6 +135,7 @@ import { MessageSimple as MessageSimpleDefault } from '../Message/MessageSimple/ import { MessageStatus as MessageStatusDefault } from '../Message/MessageSimple/MessageStatus'; import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageSimple/MessageTimestamp'; import { ReactionList as ReactionListDefault } from '../Message/MessageSimple/ReactionList'; +import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView'; import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton'; import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton'; import { AudioRecorder as AudioRecorderDefault } from '../MessageInput/components/AudioRecorder/AudioRecorder'; @@ -154,6 +155,7 @@ import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/M import { SendButton as SendButtonDefault } from '../MessageInput/SendButton'; import { SendMessageDisallowedIndicator as SendMessageDisallowedIndicatorDefault } from '../MessageInput/SendMessageDisallowedIndicator'; import { ShowThreadMessageInChannelButton as ShowThreadMessageInChannelButtonDefault } from '../MessageInput/ShowThreadMessageInChannelButton'; +import { StopMessageStreamingButton as DefaultStopMessageStreamingButton } from '../MessageInput/StopMessageStreamingButton'; import { UploadProgressIndicator as UploadProgressIndicatorDefault } from '../MessageInput/UploadProgressIndicator'; import { DateHeader as DateHeaderDefault } from '../MessageList/DateHeader'; import type { MessageType } from '../MessageList/hooks/useMessageList'; @@ -333,6 +335,7 @@ export type ChannelPropsWithContext< | 'VideoThumbnail' | 'PollContent' | 'hasCreatePoll' + | 'StreamingMessageView' > > & Partial, 'allowThreadMessagesInChannel'>> & { @@ -420,7 +423,12 @@ export type ChannelPropsWithContext< * Tells if channel is rendering a thread list */ threadList?: boolean; - } & Partial>; + } & Partial< + Pick< + InputMessageInputContextValue, + 'openPollCreationDialog' | 'CreatePollContent' | 'StopMessageStreamingButton' + > + >; const ChannelWithContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -544,7 +552,15 @@ const ChannelWithContext = < MessageAvatar = MessageAvatarDefault, MessageBounce = MessageBounceDefault, MessageContent = MessageContentDefault, - messageContentOrder = ['quoted_reply', 'gallery', 'files', 'poll', 'text', 'attachments'], + messageContentOrder = [ + 'quoted_reply', + 'gallery', + 'files', + 'poll', + 'ai_text', + 'text', + 'attachments', + ], MessageDeleted = MessageDeletedDefault, MessageEditedTimestamp = MessageEditedTimestampDefault, MessageError = MessageErrorDefault, @@ -596,6 +612,8 @@ const ChannelWithContext = < StartAudioRecordingButton = AudioRecordingButtonDefault, stateUpdateThrottleInterval = defaultThrottleInterval, StickyHeader = StickyHeaderDefault, + StopMessageStreamingButton: StopMessageStreamingButtonOverride, + StreamingMessageView = DefaultStreamingMessageView, supportedReactions = reactionData, t, thread: threadFromProps, @@ -612,6 +630,10 @@ const ChannelWithContext = < } = props; const { thread: threadProps, threadInstance } = threadFromProps; + const StopMessageStreamingButton = + StopMessageStreamingButtonOverride === undefined + ? DefaultStopMessageStreamingButton + : StopMessageStreamingButtonOverride; const { theme: { @@ -2338,6 +2360,7 @@ const ChannelWithContext = < setQuotedMessageState, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + StopMessageStreamingButton, UploadProgressIndicator, }); @@ -2439,6 +2462,7 @@ const ChannelWithContext = < setEditingState, setQuotedMessageState, shouldShowUnreadUnderlay, + StreamingMessageView, supportedReactions, targetedMessage, TypingIndicator, diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts index 5ef1125d9d..70abe0888b 100644 --- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts @@ -64,6 +64,7 @@ export const useCreateInputMessageInputContext = < showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + StopMessageStreamingButton, UploadProgressIndicator, }: InputMessageInputContextValue & { /** @@ -137,6 +138,7 @@ export const useCreateInputMessageInputContext = < showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + StopMessageStreamingButton, UploadProgressIndicator, }), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 3948abaad8..9587c9c1bd 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -88,6 +88,7 @@ export const useCreateMessagesContext = < setEditingState, setQuotedMessageState, shouldShowUnreadUnderlay, + StreamingMessageView, supportedReactions, targetedMessage, TypingIndicator, @@ -189,6 +190,7 @@ export const useCreateMessagesContext = < setEditingState, setQuotedMessageState, shouldShowUnreadUnderlay, + StreamingMessageView, supportedReactions, targetedMessage, TypingIndicator, diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx index 2e2999408c..06f0f329c5 100644 --- a/package/src/components/Chat/Chat.tsx +++ b/package/src/components/Chat/Chat.tsx @@ -100,6 +100,12 @@ export type ChatProps< * ``` */ i18nInstance?: Streami18n; + /** + * Custom loading indicator component to be used to represent the loading state of the chat. + * + * This can be used during the phase when db is not initialised. + */ + LoadingIndicator?: React.ComponentType | null; /** * You can pass the theme object to customize the styles of Chat components. You can check the default theme in [theme.ts](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/contexts/themeContext/utils/theme.ts) * @@ -143,6 +149,7 @@ const ChatWithContext = < enableOfflineSupport = false, i18nInstance, ImageComponent = Image, + LoadingIndicator = null, resizableCDNHosts = ['.stream-io-cdn.com'], style, } = props; @@ -257,7 +264,7 @@ const ChatWithContext = < if (userID && enableOfflineSupport && !initialisedDatabase) { // if user id has been set and offline support is enabled, we need to wait for database to be initialised - return null; + return LoadingIndicator ? : null; } return ( diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index ce5b4f700d..bc5d1cadea 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -456,6 +456,8 @@ const MessageWithContext = < return !!attachments.images.length || !!attachments.videos.length; case 'poll': return !!message.poll_id; + case 'ai_text': + return !!message.ai_generated; case 'text': default: return !!message.text; @@ -863,7 +865,8 @@ const areEqual = & Pick & { setMessageContentWidth: React.Dispatch>; @@ -142,6 +143,7 @@ const MessageContentWithContext = < Reply, setMessageContentWidth, showMessageStatus, + StreamingMessageView, threadList, } = props; const { client } = useChatContext(); @@ -393,9 +395,16 @@ const MessageContentWithContext = < /> ) : null; } + case 'ai_text': + return message.ai_generated ? ( + + ) : null; case 'text': default: - return otherAttachments.length && otherAttachments[0].actions ? null : ( + return (otherAttachments.length && otherAttachments[0].actions) || + message.ai_generated ? null : ( key={`message_text_container_${messageContentOrderIndex}`} /> @@ -484,7 +493,8 @@ const areEqual = (); const { t } = useTranslationContext(); @@ -635,6 +646,7 @@ export const MessageContent = < preventPress, Reply, showMessageStatus, + StreamingMessageView, t, threadList, }} diff --git a/package/src/components/Message/MessageSimple/MessageFooter.tsx b/package/src/components/Message/MessageSimple/MessageFooter.tsx index b16e2dcf6f..0cf33cbb31 100644 --- a/package/src/components/Message/MessageSimple/MessageFooter.tsx +++ b/package/src/components/Message/MessageSimple/MessageFooter.tsx @@ -129,6 +129,8 @@ const MessageFooterWithContext = < return null; } + const isEdited = isEditedMessage(message); + return ( <> @@ -141,7 +143,7 @@ const MessageFooterWithContext = < {showMessageStatus && } - {isEditedMessage(message) && !isEditedMessageOpen && ( + {isEdited && !isEditedMessageOpen ? ( <> ('Edited')} - )} + ) : null} - {isEditedMessageOpen && ( + {isEdited && isEditedMessageOpen ? ( - )} + ) : null} ); }; diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx index 4bd5bd0b95..ea3a66d27b 100644 --- a/package/src/components/Message/MessageSimple/MessageSimple.tsx +++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx @@ -154,7 +154,9 @@ const areEqual = = Pick, 'message'> & { + letterInterval?: number; + renderingLetterCount?: number; +}; + +export const StreamingMessageView = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: StreamingMessageViewProps, +) => { + const { letterInterval, message: messageFromProps, renderingLetterCount } = props; + const { message: messageFromContext } = useMessageContext(); + const message = messageFromProps || messageFromContext; + const { text = '' } = message; + const { streamedMessageText } = useStreamingMessage({ + letterInterval, + renderingLetterCount, + text, + }); + + return ; +}; + +StreamingMessageView.displayName = 'StreamingMessageView{messageSimple{content}}'; diff --git a/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts b/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts index 4e116cc4f2..dd654facb1 100644 --- a/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts +++ b/package/src/components/Message/MessageSimple/utils/generateMarkdownText.ts @@ -33,7 +33,11 @@ export const generateMarkdownText = (text?: string) => { resultText = resultText.replace(mentionsRegex, `@${displayLink}`); } - resultText = resultText.replace(/[<"'>]/g, '\\$&'); + // Escape the " and ' characters, except in code blocks where we deem this allowed. + resultText = resultText.replace(/(```[\s\S]*?```|`.*?`)|[<"'>]/g, (match, code) => { + if (code) return code; + return `\\${match}`; + }); // Remove whitespaces that come directly after newlines except in code blocks where we deem this allowed. resultText = resultText.replace(/(```[\s\S]*?```|`.*?`)|\n[ ]{2,}/g, (_, code) => { @@ -41,5 +45,9 @@ export const generateMarkdownText = (text?: string) => { return '\n'; }); + // 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 + // items for example were broken. We clean up the code block closing state within the rendering itself. + resultText = resultText.replace(/\n```/g, '\n\n```'); + return resultText; }; diff --git a/package/src/components/Message/MessageSimple/utils/renderText.tsx b/package/src/components/Message/MessageSimple/utils/renderText.tsx index 24a52eadb7..ab42170908 100644 --- a/package/src/components/Message/MessageSimple/utils/renderText.tsx +++ b/package/src/components/Message/MessageSimple/utils/renderText.tsx @@ -1,8 +1,18 @@ -import React, { PropsWithChildren } from 'react'; -import { GestureResponderEvent, Linking, Text, TextProps, View, ViewProps } from 'react-native'; - +import React, { PropsWithChildren, ReactNode, useCallback, useMemo } from 'react'; +import { + GestureResponderEvent, + Linking, + Platform, + Text, + TextProps, + View, + ViewProps, +} from 'react-native'; + +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; // @ts-expect-error import Markdown from 'react-native-markdown-package'; +import Animated, { clamp, scrollTo, useAnimatedRef, useSharedValue } from 'react-native-reanimated'; import { DefaultRules, @@ -26,7 +36,64 @@ import type { DefaultStreamChatGenerics } from '../../../../types/types'; import { escapeRegExp } from '../../../../utils/utils'; import type { MessageType } from '../../../MessageList/hooks/useMessageList'; +export const MarkdownReactiveScrollView = ({ children }: { children: ReactNode }) => { + const scrollViewRef = useAnimatedRef(); + const contentWidth = useSharedValue(0); + const visibleContentWidth = useSharedValue(0); + const offsetBeforeScroll = useSharedValue(0); + + const panGesture = Gesture.Pan() + .activeOffsetX([-5, 5]) + .onUpdate((event) => { + const { translationX } = event; + + scrollTo(scrollViewRef, offsetBeforeScroll.value - translationX, 0, false); + }) + .onEnd((event) => { + const { translationX } = event; + + const velocityEffect = event.velocityX * 0.3; + + const finalPosition = clamp( + offsetBeforeScroll.value - translationX - velocityEffect, + 0, + contentWidth.value - visibleContentWidth.value, + ); + + offsetBeforeScroll.value = finalPosition; + + scrollTo(scrollViewRef, finalPosition, 0, true); + }); + + return ( + + + { + contentWidth.value = width; + }} + onLayout={(e) => { + visibleContentWidth.value = e.nativeEvent.layout.width; + }} + ref={scrollViewRef} + scrollEnabled={false} + > + {children} + + + + ); +}; + const defaultMarkdownStyles: MarkdownStyle = { + codeBlock: { + fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace', + fontWeight: '500', + marginVertical: 8, + }, inlineCode: { fontSize: 13, padding: 3, @@ -60,6 +127,26 @@ const defaultMarkdownStyles: MarkdownStyle = { marginBottom: 8, marginTop: 8, }, + table: { + borderRadius: 3, + borderWidth: 1, + flex: 1, + flexDirection: 'row', + }, + tableHeader: { + flexDirection: 'row', + justifyContent: 'space-around', + }, + tableHeaderCell: { + fontWeight: '500', + }, + tableRow: { + alignItems: 'center', + justifyContent: 'space-around', + }, + tableRowCell: { + flex: 1, + }, }; const mentionsParseFunction: ParseFunction = (capture, parse, state) => ({ @@ -113,6 +200,13 @@ export const renderText = < color: colors.accent_blue, ...markdownStyles?.autolink, }, + codeBlock: { + ...defaultMarkdownStyles.codeBlock, + backgroundColor: colors.code_block, + color: colors.black, + padding: 8, + ...markdownStyles?.codeBlock, + }, inlineCode: { ...defaultMarkdownStyles.inlineCode, backgroundColor: colors.white_smoke, @@ -125,6 +219,35 @@ export const renderText = < color: colors.accent_blue, ...markdownStyles?.mentions, }, + table: { + ...defaultMarkdownStyles.table, + borderColor: colors.grey_dark, + marginVertical: 8, + ...markdownStyles?.table, + }, + tableHeader: { + ...defaultMarkdownStyles.tableHeader, + backgroundColor: colors.grey, + ...markdownStyles?.tableHeader, + }, + tableHeaderCell: { + ...defaultMarkdownStyles.tableHeaderCell, + padding: 5, + ...markdownStyles?.tableHeaderCell, + }, + tableRow: { + ...defaultMarkdownStyles.tableRow, + ...markdownStyles?.tableRow, + }, + tableRowCell: { + ...defaultMarkdownStyles.tableRowCell, + borderColor: colors.grey_dark, + padding: 5, + ...markdownStyles?.tableRowCell, + }, + tableRowLast: { + ...markdownStyles?.tableRowLast, + }, text: { ...defaultMarkdownStyles.text, color: colors.black, @@ -263,6 +386,18 @@ export const renderText = < /> ); + const codeBlockReact: ReactNodeOutput = (node, _, state) => ( + + {node?.content?.trim()} + + ); + + const tableReact: ReactNodeOutput = (node, output, state) => ( + + + + ); + const customRules = { // do not render images, we will scrape them out of the message and show on attachment card component image: { match: () => null }, @@ -283,6 +418,8 @@ export const renderText = < }, } : {}), + codeBlock: { react: codeBlockReact }, + table: { react: tableReact }, }; return ( @@ -373,3 +510,70 @@ const ListRow = ({ children, style }: PropsWithChildren) => ( const ListItem = ({ children, style }: PropsWithChildren) => ( {children} ); + +export type MarkdownTableProps = { + node: SingleASTNode; + output: ReactOutput; + state: State; + styles: Partial; +}; + +const transpose = (matrix: SingleASTNode[][]) => + matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex])); + +const MarkdownTable = ({ node, output, state, styles }: MarkdownTableProps) => { + const content = useMemo(() => { + const nodeContent = [node?.header, ...node?.cells]; + return transpose(nodeContent); + }, [node?.cells, node?.header]); + const columns = content?.map((column, idx) => ( + + )); + + return ( + + {columns} + + ); +}; + +export type MarkdownTableRowProps = { + items: SingleASTNode[]; + output: ReactOutput; + state: State; + styles: Partial; +}; + +const MarkdownTableColumn = ({ items, output, state, styles }: MarkdownTableRowProps) => { + const [headerCellContent, ...columnCellContents] = items; + + const ColumnCell = useCallback( + ({ content }: { content: SingleASTNode }) => + content ? ( + + {output(content, state)} + + ) : null, + [output, state, styles], + ); + + return ( + + {headerCellContent ? ( + + {output(headerCellContent, state)} + + ) : null} + {columnCellContents && + columnCellContents.map((content, idx) => ( + + ))} + + ); +}; diff --git a/package/src/components/Message/hooks/useStreamingMessage.ts b/package/src/components/Message/hooks/useStreamingMessage.ts new file mode 100644 index 0000000000..793c632c6f --- /dev/null +++ b/package/src/components/Message/hooks/useStreamingMessage.ts @@ -0,0 +1,54 @@ +import { useEffect, useRef, useState } from 'react'; + +import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { StreamingMessageViewProps } from '../MessageSimple/StreamingMessageView'; + +export type UseStreamingMessageProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick< + StreamingMessageViewProps, + 'letterInterval' | 'renderingLetterCount' +> & { text: string }; + +const DEFAULT_LETTER_INTERVAL = 0; +const DEFAULT_RENDERING_LETTER_COUNT = 2; + +/** + * A hook that returns text in a streamed, typewriter fashion. The speed of streaming is + * configurable. + * @param {number} [letterInterval=0] - The timeout between each typing animation in milliseconds. + * @param {number} [renderingLetterCount=2] - The number of letters to be rendered each time we update. + * @param {string} text - The text that we want to render in a typewriter fashion. + * @returns {{ streamedMessageText: string }} - A substring of the text property, up until we've finished rendering the typewriter animation. + */ +export const useStreamingMessage = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + letterInterval = DEFAULT_LETTER_INTERVAL, + renderingLetterCount = DEFAULT_RENDERING_LETTER_COUNT, + text, +}: UseStreamingMessageProps): { streamedMessageText: string } => { + const [streamedMessageText, setStreamedMessageText] = useState(text); + const textCursor = useRef(text.length); + + useEffect(() => { + const textLength = text.length; + const interval = setInterval(() => { + if (!text || textCursor.current >= textLength) { + clearInterval(interval); + } + const newCursorValue = textCursor.current + renderingLetterCount; + const newText = text.substring(0, newCursorValue); + textCursor.current += newText.length - textCursor.current; + const codeBlockCounts = (newText.match(/```/g) || []).length; + const shouldOptimisticallyCloseCodeBlock = codeBlockCounts > 0 && codeBlockCounts % 2 > 0; + setStreamedMessageText(shouldOptimisticallyCloseCodeBlock ? newText + '```' : newText); + }, letterInterval); + + return () => { + clearInterval(interval); + }; + }, [letterInterval, renderingLetterCount, text]); + + return { streamedMessageText }; +}; diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index 2a5eb598c2..d7985e4a3b 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Modal, NativeSyntheticEvent, @@ -58,6 +58,7 @@ import { import { isImageMediaLibraryAvailable, triggerHaptic } from '../../native'; import type { Asset, DefaultStreamChatGenerics } from '../../types/types'; +import { AIStates, useAIState } from '../AITypingIndicatorView'; import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput'; import { CreatePoll } from '../Poll/CreatePollContent'; @@ -159,6 +160,7 @@ type MessageInputPropsWithContext< | 'showPollCreationDialog' | 'sendMessage' | 'CreatePollContent' + | 'StopMessageStreamingButton' > & Pick, 'Reply'> & Pick< @@ -228,6 +230,7 @@ const MessageInputWithContext = < showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, + StopMessageStreamingButton, suggestions, text, thread, @@ -728,6 +731,13 @@ const MessageInputWithContext = < })), }; + const { channel } = useChannelContext(); + const { aiState } = useAIState(channel); + + const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]); + const shouldDisplayStopAIGeneration = + [AIStates.Thinking, AIStates.Generating].includes(aiState) && !!StopMessageStreamingButton; + return ( <> )} - {isSendingButtonVisible() && + {shouldDisplayStopAIGeneration ? ( + + ) : ( + isSendingButtonVisible() && (cooldownRemainingSeconds ? ( ) : ( @@ -842,7 +855,8 @@ const MessageInputWithContext = < disabled={sending.current || !isValidMessage() || (giphyActive && !isOnline)} /> - ))} + )) + )} {audioRecordingEnabled && !micLocked && ( void; +}; + +export const StopMessageStreamingButton = (props: StopMessageStreamingButtonProps) => { + const { onPress } = props; + + const { + theme: { + colors: { accent_blue }, + messageInput: { stopMessageStreamingButton, stopMessageStreamingIcon }, + }, + } = useTheme(); + + return ( + + + + ); +}; + +StopMessageStreamingButton.displayName = 'StopMessageStreamingButton{messageInput}'; diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx index 5b75f92165..a168bff155 100644 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ b/package/src/components/MessageOverlay/MessageOverlay.tsx @@ -45,6 +45,7 @@ import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContex import { useViewport } from '../../hooks/useViewport'; import type { DefaultStreamChatGenerics } from '../../types/types'; import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer'; +import { StreamingMessageView } from '../Message/MessageSimple/StreamingMessageView'; import { OverlayReactions as DefaultOverlayReactions } from '../MessageOverlay/OverlayReactions'; import type { ReplyProps } from '../Reply/Reply'; @@ -451,9 +452,17 @@ const MessageOverlayWithContext = < ) : null; } + case 'ai_text': + return ( + + ); case 'text': default: - return otherAttachments?.length && otherAttachments[0].actions ? null : ( + return (otherAttachments?.length && otherAttachments[0].actions) || + message.ai_generated ? null : ( key={`message_text_container_${messageContentOrderIndex}`} message={message} diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 9e94441424..63fc7d307b 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -97,6 +97,7 @@ export * from './KeyboardCompatibleView/KeyboardCompatibleView'; export * from './Message/hooks/useCreateMessageContext'; export * from './Message/hooks/useMessageActions'; export * from './Message/hooks/useMessageActionHandlers'; +export * from './Message/hooks/useStreamingMessage'; export * from './Message/Message'; export * from './Message/MessageSimple/MessageAvatar'; export * from './Message/MessageSimple/MessageBounce'; @@ -132,6 +133,7 @@ export * from './MessageInput/InputButtons'; export * from './MessageInput/MessageInput'; export * from './MessageInput/MoreOptionsButton'; export * from './MessageInput/SendButton'; +export * from './MessageInput/StopMessageStreamingButton'; export * from './MessageInput/ShowThreadMessageInChannelButton'; export * from './MessageInput/UploadProgressIndicator'; @@ -172,3 +174,6 @@ export * from './Spinner/Spinner'; export * from './Thread/Thread'; export * from './Thread/components/ThreadFooterComponent'; export * from './ThreadList/ThreadList'; + +export * from './Message/MessageSimple/StreamingMessageView'; +export * from './AITypingIndicatorView'; diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 77576e0721..a84bf72413 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -29,7 +29,7 @@ import { useMessageDetailsForState } from './hooks/useMessageDetailsForState'; import { isUploadAllowed, MAX_FILE_SIZE_TO_UPLOAD, prettifyFileSize } from './utils/utils'; -import { PollContentProps } from '../../components'; +import { PollContentProps, StopMessageStreamingButtonProps } from '../../components'; import { AudioAttachmentProps } from '../../components/Attachment/AudioAttachment'; import { parseLinksFromText } from '../../components/Message/MessageSimple/utils/parseLinks'; import type { AttachButtonProps } from '../../components/MessageInput/AttachButton'; @@ -385,6 +385,7 @@ export type InputMessageInputContextValue< * 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) */ StartAudioRecordingButton: React.ComponentType>; + StopMessageStreamingButton: React.ComponentType; /** * Custom UI component to render upload progress indicator on attachment preview. * @@ -586,7 +587,9 @@ export const MessageInputProvider = < editing, initialValue, openPollCreationDialog: openPollCreationDialogFromContext, + StopMessageStreamingButton, } = value; + const { fileUploads, imageUploads, @@ -1481,6 +1484,7 @@ export const MessageInputProvider = < openPollCreationDialog, sendMessage, // overriding the originally passed in sendMessage showPollCreationDialog, + StopMessageStreamingButton, }); return ( Promise; setEditingState: (message?: MessageType) => void; setQuotedMessageState: (message: MessageType | boolean) => void; + /** + * UI component for StreamingMessageView. Displays the text of a message with a typewriter animation. + */ + StreamingMessageView: React.ComponentType>; supportedReactions: ReactionData[]; /** * UI component for TypingIndicator diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 9e380e064c..d69b93fc29 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -18,6 +18,7 @@ export const Colors = { black: '#000000', blue_alice: '#E9F2FF', border: '#00000014', // 14 = 8% opacity; top: x=0, y=-1; bottom: x=0, y=1 + code_block: '#DDDDDD', disabled: '#B4BBBA', grey: '#7A7A7A', grey_dark: '#72767E', @@ -79,7 +80,7 @@ export type MarkdownStyle = Partial<{ tableHeader: ViewStyle; tableHeaderCell: TextStyle; tableRow: ViewStyle; - tableRowCell: ViewStyle; + tableRowCell: TextStyle; tableRowLast: ViewStyle; text: TextStyle; u: TextStyle; @@ -87,6 +88,10 @@ export type MarkdownStyle = Partial<{ }>; export type Theme = { + aiTypingIndicatorView: { + container: ViewStyle; + text: TextStyle; + }; attachmentPicker: { bottomSheetContentContainer: ViewStyle; durationText: TextStyle; @@ -351,6 +356,8 @@ export type Theme = { innerContainer: ViewStyle; text: TextStyle; }; + stopMessageStreamingButton: ViewStyle; + stopMessageStreamingIcon: IconProps; suggestions: { command: { args: TextStyle; @@ -792,6 +799,10 @@ export type Theme = { }; export const defaultTheme: Theme = { + aiTypingIndicatorView: { + container: {}, + text: {}, + }, attachmentPicker: { bottomSheetContentContainer: {}, durationText: {}, @@ -1062,6 +1073,8 @@ export const defaultTheme: Theme = { innerContainer: {}, text: {}, }, + stopMessageStreamingButton: {}, + stopMessageStreamingIcon: {}, suggestions: { command: { args: {}, diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index eb3c96f8ad..dd0fd419c7 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -38,6 +38,7 @@ "Flag": "Flag", "Flag Message": "Flag Message", "Flag action failed either due to a network issue or the message is already flagged": "Flag action failed either due to a network issue or the message is already flagged.", + "Generating...": "Generating...", "Hold to start recording.": "Hold to start recording.", "How about sending your first message to a friend?": "How about sending your first message to a friend?", "Instant Commands": "Instant Commands", @@ -95,6 +96,7 @@ "Suggest an option": "Suggest an option", "The message has been reported to a moderator.": "The message has been reported to a moderator.", "The source message was deleted": "The source message was deleted", + "Thinking...": "Thinking...", "This is already an option": "This is already an option", "This reply was deleted": "This reply was deleted", "Thread Reply": "Thread Reply", diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 576a1778dc..d8fe2ba9f7 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -38,6 +38,7 @@ "Flag": "Reportar", "Flag Message": "Reportar mensaje", "Flag action failed either due to a network issue or the message is already flagged": "El reporte falló debido a un problema de red o el mensaje ya fue reportado.", + "Generating...": "Generando...", "Hold to start recording.": "Mantén presionado para comenzar a grabar.", "How about sending your first message to a friend?": "¿Qué tal enviar tu primer mensaje a un amigo?", "Instant Commands": "Comandos instantáneos", @@ -97,6 +98,7 @@ "Suggest an option": "Sugerir una opción", "The message has been reported to a moderator.": "El mensaje ha sido reportado a un moderador.", "The source message was deleted": "El mensaje original fue eliminado", + "Thinking...": "Pensando...", "This is already an option": "Esto ya es una opción", "This reply was deleted": "Esta respuesta fue eliminada", "Thread Reply": "Respuesta de hilo", diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index 06afa66719..ab4d3bf426 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -38,6 +38,7 @@ "Flag": "Signaler", "Flag Message": "Signaler le message", "Flag action failed either due to a network issue or the message is already flagged": "L'action de signalisation a échoué en raison d'un problème de réseau ou le message est déjà signalé.", + "Generating...": "Génération...", "Hold to start recording.": "Hold to start recording.", "How about sending your first message to a friend?": "Et si vous envoyiez votre premier message à un ami ?", "Instant Commands": "Commandes Instantanées", @@ -97,6 +98,7 @@ "Suggest an option": "Suggérer une option", "The message has been reported to a moderator.": "Le message a été signalé à un modérateur.", "The source message was deleted": "Le message source a été supprimé", + "Thinking...": "Réflexion...", "This is already an option": "C'est déjà une option", "This reply was deleted": "Cette réponse a été supprimée", "Thread Reply": "Réponse à la discussion", diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index 48f3b0df02..b2bf283eee 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -38,6 +38,7 @@ "Flag": "סמן", "Flag Message": "סמן הודעה", "Flag action failed either due to a network issue or the message is already flagged": "פעולת הסימון נכשלה בגלל בעיית רשת או שההודעה כבר סומנה.", + "Generating...": "מייצר...", "Hold to start recording.": "לחץ והחזק כדי להתחיל להקליט.", "How about sending your first message to a friend?": "מה דעתך לשלוח את ההודעה הראשונה שלך לחבר?", "Instant Commands": "פעולות מיידיות", @@ -97,6 +98,7 @@ "Suggest an option": "הצע אפשרות", "The message has been reported to a moderator.": "ההודעה דווחה למנהל", "The source message was deleted": "ההודעה המקורית נמחקה", + "Thinking...": "חושב...", "This is already an option": "זו כבר אפשרות קיימת", "This reply was deleted": "התגובה הזו נמחקה", "Thread Reply": "הגב/י בשרשור", diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index a65b30cf97..a7bdd4f5ae 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -38,6 +38,7 @@ "Flag": "झंडा", "Flag Message": "झंडा संदेश", "Flag action failed either due to a network issue or the message is already flagged": "फ़्लैग कार्रवाई या तो नेटवर्क समस्या के कारण विफल हो गई या संदेश पहले से फ़्लैग किया गया है।", + "Generating...": "जनरेट कर रहा है...", "Hold to start recording.": "रिकॉर्डिंग शुरू करने के लिए दबाएं।", "How about sending your first message to a friend?": "किसी मित्र को अपना पहला संदेश भेजने के बारे में क्या ख़याल है?", "Instant Commands": "त्वरित कमांड", @@ -95,6 +96,7 @@ "Suggest an option": "एक विकल्प सुझाएं", "The message has been reported to a moderator.": "संदेश एक मॉडरेटर को सूचित किया गया है।", "The source message was deleted": "स्रोत संदेश हटा दिया गया है", + "Thinking...": "सोच रहा है...", "This is already an option": "यह पहले से एक विकल्प है", "This reply was deleted": "यह उत्तर हटा दिया गया है", "Thread Reply": "धागा जवाब", diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index 46799dd420..e6621a2ed3 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -38,6 +38,7 @@ "Flag": "Contrassegna", "Flag Message": "Contrassegna Messaggio", "Flag action failed either due to a network issue or the message is already flagged": "L'azione di segnalazione non è riuscita a causa di un problema di rete o il messaggio è già segnalato.", + "Generating...": "Generando...", "Hold to start recording.": "Tieni premuto per avviare la registrazione.", "How about sending your first message to a friend?": "Che ne dici di inviare il tuo primo messaggio ad un amico?", "Instant Commands": "Comandi Istantanei", @@ -97,6 +98,7 @@ "Suggest an option": "Suggerisci un'opzione", "The message has been reported to a moderator.": "Il messaggio è stato segnalato a un moderatore.", "The source message was deleted": "Il messaggio originale è stato eliminato", + "Thinking...": "Pensando...", "This is already an option": "Questa è già un'opzione", "This reply was deleted": "Questa risposta è stata eliminata", "Thread Reply": "Rispondi alla Discussione", diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index cd3a540baf..047978bdf9 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -38,6 +38,7 @@ "Flag": "フラグ", "Flag Message": "メッセージをフラグする", "Flag action failed either due to a network issue or the message is already flagged": "ネットワーク接続に問題があるか、すでにフラグが設定されているため、フラグが失敗しました。", + "Generating...": "生成中...", "Hold to start recording.": "録音を開始するには押し続けてください。", "How about sending your first message to a friend?": "初めてのメッセージを友達に送ってみてはいかがでしょうか?", "Instant Commands": "インスタントコマンド", @@ -95,6 +96,7 @@ "Suggest an option": "オプションを提案", "The message has been reported to a moderator.": "メッセージはモデレーターに報告されました。", "The source message was deleted": "元のメッセージが削除されました", + "Thinking...": "考え中...", "This is already an option": "これはすでにオプションです", "This reply was deleted": "この返信は削除されました", "Thread Reply": "スレッドの返信", diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index 5ab6624773..5ecab344c7 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -38,6 +38,7 @@ "Flag": "플래그", "Flag Message": "메시지를 플래그하기", "Flag action failed either due to a network issue or the message is already flagged": "네트워크 연결에 문제가 있거나 이미 플래그 되어서 플래그에 실패했습니다.", + "Generating...": "생성 중...", "Hold to start recording.": "녹음을 시작하려면 눌러주세요.", "How about sending your first message to a friend?": "친구에게 첫 번째 메시지를 보내는 것은 어떻습니까?", "Instant Commands": "인스턴트 명령", @@ -95,6 +96,7 @@ "Suggest an option": "옵션 제안", "The message has been reported to a moderator.": "메시지는 운영자에보고되었습니다.", "The source message was deleted": "원본 메시지가 삭제되었습니다", + "Thinking...": "생각 중...", "This is already an option": "이미 존재하는 옵션입니다", "This reply was deleted": "이 답글은 삭제되었습니다", "Thread Reply": "스레드\u3000답장", diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 258c6dbf2f..ce43d7f80a 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -38,6 +38,7 @@ "Flag": "Markeer", "Flag Message": "Markeer bericht", "Flag action failed either due to a network issue or the message is already flagged": "Rapporteren mislukt door een netwerk fout of het berich is al gerapporteerd", + "Generating...": "Aan het genereren...", "Hold to start recording.": "Houd vast om opname te starten.", "How about sending your first message to a friend?": "Wat dacht je ervan om je eerste bericht naar een vriend te sturen?", "Instant Commands": "Directe Opdrachten", @@ -95,6 +96,7 @@ "Suggest an option": "Stel een optie voor", "The message has been reported to a moderator.": "Het bericht is gerapporteerd aan een moderator.", "The source message was deleted": "Het oorspronkelijke bericht is verwijderd", + "Thinking...": "Aan het denken...", "This is already an option": "Dit is al een optie", "This reply was deleted": "Deze reactie is verwijderd", "Thread Reply": "Discussie beantwoorden", diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index 2d4ff45565..c6f24b0c2d 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -38,6 +38,7 @@ "Flag": "Reportar", "Flag Message": "Reportar Mensagem", "Flag action failed either due to a network issue or the message is already flagged": "A ação para reportar a mensagem falhou devido a um problema de rede ou a mensagem já foi reportada.", + "Generating...": "Gerando...", "Hold to start recording.": "Mantenha pressionado para começar a gravar.", "How about sending your first message to a friend?": "Que tal enviar sua primeira mensagem para um amigo?", "Instant Commands": "Comandos Instantâneos", @@ -97,6 +98,7 @@ "Suggest an option": "Sugerir uma opção", "The message has been reported to a moderator.": "A mensagem foi relatada a um moderador.", "The source message was deleted": "A mensagem original foi excluída", + "Thinking...": "Pensando...", "This is already an option": "Isso já é uma opção", "This reply was deleted": "Esta resposta foi excluída", "Thread Reply": "Respostas de Tópico", diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index fa2f946ea2..6ad23a565d 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -38,6 +38,7 @@ "Flag": "Пометить", "Flag Message": "Пометить сообщение", "Flag action failed either due to a network issue or the message is already flagged": "Не удалось отправить жалобу. Возможные причины: проблема с подключением к интернету или ваша жалоба уже была принята.", + "Generating...": "Генерирую...", "Hold to start recording.": "Удерживайте, чтобы начать запись.", "How about sending your first message to a friend?": "Как насчет отправки первого сообщения другу?", "Instant Commands": "Мгновенные Команды", @@ -99,6 +100,7 @@ "Suggest an option": "Предложить вариант", "The message has been reported to a moderator.": "Сообщение отправлено модератору.", "The source message was deleted": "Исходное сообщение было удалено", + "Thinking...": "Думаю...", "This is already an option": "Это уже вариант", "This reply was deleted": "Этот ответ был удалён", "Thread Reply": "Тема Ответить", diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index 4493eabbef..f1bd61a28c 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -38,6 +38,7 @@ "Flag": "Raporla", "Flag Message": "Mesajı Raporla", "Flag action failed either due to a network issue or the message is already flagged": "Mesajın daha önce raporlanmış olması veya bir ağ bağlantısı sorunu nedeniyle raporlama işlemi başarısız oldu.", + "Generating...": "Oluşturuluyor...", "Hold to start recording.": "Kayıt yapmak için basılı tutun.", "How about sending your first message to a friend?": "İlk mesajınızı bir arkadaşınıza göndermeye ne dersiniz?", "Instant Commands": "Anlık Komutlar", @@ -95,6 +96,7 @@ "Suggest an option": "Bir seçenek öner", "The message has been reported to a moderator.": "Mesaj moderatöre bildirildi.", "The source message was deleted": "Kaynak mesaj silindi", + "Thinking...": "Düşünüyor...", "This is already an option": "Bu zaten bir seçenek", "This reply was deleted": "Bu yanıt silindi", "Thread Reply": "Konu Yanıtı", diff --git a/package/src/utils/getTrimmedAttachmentTitle.ts b/package/src/utils/getTrimmedAttachmentTitle.ts index 59079851b0..e798d5f4cd 100644 --- a/package/src/utils/getTrimmedAttachmentTitle.ts +++ b/package/src/utils/getTrimmedAttachmentTitle.ts @@ -1,5 +1,13 @@ +import { lookup } from 'mime-types'; + export const getTrimmedAttachmentTitle = (title?: string) => { if (!title) return ''; - const lastIndexOfDot = title.lastIndexOf('.'); - return title.length < 12 ? title : title.slice(0, 12) + '...' + title.slice(lastIndexOfDot); + + const mimeType = lookup(title); + if (mimeType) { + const lastIndexOfDot = title.lastIndexOf('.'); + return title.length < 12 ? title : title.slice(0, 12) + '...' + title.slice(lastIndexOfDot); + } else { + return title; + } }; diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index 009ba25852..18434107aa 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -108,7 +108,7 @@ export const isEditedMessage = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( message: MessageType, -) => !!message.message_text_updated_at; +) => !!message.message_text_updated_at && !message.ai_generated; /** * Default emoji search index for auto complete text input diff --git a/package/yarn.lock b/package/yarn.lock index 056f9600ae..ae9e60b0c0 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -10664,10 +10664,10 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-chat@8.45.1: - version "8.45.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.45.1.tgz#001f452520602ebffe45f1634b5f5dd1ff036ec5" - integrity sha512-7OMpL2RHUd+PXSWzhTUAIjjXlI9Oqc4HhUBRfc5i6dK+Ug9S5ertb7RvyzGL5N4ITpq/6ZUAwXtTqRaN9+UUkw== +stream-chat@8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.46.0.tgz#416b325e05b144d0937a3527d1e622463113d605" + integrity sha512-HQVCRVldrfQFAvsBOHiHR0TKYf+wpsg/cAzRojeZY+buy1vG6eoqk09h6Fl4k2eG3zFLoA0G9W6o7o45jyFE1g== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0"