diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index 4bdc24765..60f518b38 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -196,6 +196,9 @@ const App = () => { drafts: { enabled: true, }, + linkPreviews: { + enabled: true, + } }); setupCommandUIMiddlewares(composer); diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index 784af4a07..e365c6aa1 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -8349,10 +8349,10 @@ stream-chat-react-native-core@8.1.0: version "0.0.0" uid "" -stream-chat@^9.27.2: - version "9.27.2" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.27.2.tgz#5b41173e513f3606c47c93f391693b589e663968" - integrity sha512-OdALDzg8lO8CAdl8deydJ1+O4wJ7mM9dPLeCwDppq/OQ4aFIS9X38P+IdXPcOCsgSS97UoVUuxD2/excC5PEeg== +stream-chat@^9.30.1: + version "9.30.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.30.1.tgz#86d152e4d0894854370512d17530854541f7990b" + integrity sha512-8f58tCo3QfgzaNhWHpRQzEfglSPPn4lGRn74FFTr/pn53dMJwtcKDSohV6NTHBrkYWTXYObRnHgh2IhGFUKckw== dependencies: "@types/jsonwebtoken" "^9.0.8" "@types/ws" "^8.5.14" diff --git a/package/package.json b/package/package.json index 57645fa6d..75a84d386 100644 --- a/package/package.json +++ b/package/package.json @@ -80,7 +80,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", - "stream-chat": "^9.27.2", + "stream-chat": "^9.30.1", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index cc62e6e60..80636d8ca 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -16,9 +16,12 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { type MessageComposerState, type TextComposerState, type UserResponse } from 'stream-chat'; +import { LinkPreviewList } from './components/LinkPreviewList'; import { OutputButtons } from './components/OutputButtons'; import { useCountdown } from './hooks/useCountdown'; +import { useHasLinkPreviews } from './hooks/useLinkPreviews'; + import { ChatContextValue, useAttachmentManagerState, @@ -269,6 +272,9 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector); const { height } = useStateStore(messageInputHeightStore.store, messageInputHeightStoreSelector); + + const hasLinkPreviews = useHasLinkPreviews(); + const { theme: { semantics, @@ -411,12 +417,10 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { {Input ? ( ) : ( - + {isRecordingStateIdle ? ( - { ]} > {InputButtons && } - + ) : null} - { ) : null} {isRecordingStateIdle ? ( - { ) : null} - + + ) : null} - + {!isRecordingStateIdle ? ( ) : ( @@ -499,10 +511,10 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => { ) : null} - + - - + + )} diff --git a/package/src/components/MessageInput/components/LinkPreviewList.tsx b/package/src/components/MessageInput/components/LinkPreviewList.tsx new file mode 100644 index 000000000..61c9f784c --- /dev/null +++ b/package/src/components/MessageInput/components/LinkPreviewList.tsx @@ -0,0 +1,162 @@ +import React, { useCallback, useMemo } from 'react'; + +import { View, Text, StyleSheet } from 'react-native'; + +import Animated, { LinearTransition } from 'react-native-reanimated'; + +import type { LinkPreview } from 'stream-chat'; +import { LinkPreviewsManager } from 'stream-chat'; + +import { AttachmentRemoveControl } from './AttachmentPreview/AttachmentRemoveControl'; + +import { useChatContext, useMessageComposer, useTheme } from '../../../contexts'; +import { NewLink } from '../../../icons/NewLink'; +import { components, primitives } from '../../../theme'; +import { useLinkPreviews } from '../hooks/useLinkPreviews'; + +export type LinkPreviewListProps = { + displayLinkCount?: number; +}; + +export const LinkPreviewList = ({ displayLinkCount = 1 }: LinkPreviewListProps) => { + const linkPreviews = useLinkPreviews(); + + if (linkPreviews.length === 0) return null; + + return ( + <> + {linkPreviews.slice(0, displayLinkCount).map((linkPreview) => ( + + ))} + + ); +}; + +type LinkPreviewProps = { + linkPreview: LinkPreview; +}; + +export const LinkPreviewCard = ({ linkPreview }: LinkPreviewProps) => { + const styles = useStyles(); + const { ImageComponent } = useChatContext(); + const { linkPreviewsManager } = useMessageComposer(); + const { image_url, thumb_url, title, text, og_scrape_url } = linkPreview; + + const dismissPreview = useCallback( + () => linkPreviewsManager.dismissPreview(og_scrape_url), + [linkPreviewsManager, og_scrape_url], + ); + + if ( + !LinkPreviewsManager.previewIsLoaded(linkPreview) && + !LinkPreviewsManager.previewIsLoading(linkPreview) + ) { + return null; + } + + return ( + + + + + + + {title ? ( + + {title} + + ) : null} + {text ? ( + + {text} + + ) : null} + {og_scrape_url ? ( + + + + {og_scrape_url} + + + ) : null} + + + + + + + ); +}; + +const useStyles = () => { + const { + theme: { + semantics, + messageInput: { linkPreviewList }, + }, + } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + linkContainer: { + flexDirection: 'row', + ...linkPreviewList.linkContainer, + }, + linkIcon: { alignSelf: 'center', marginRight: 4, ...linkPreviewList.linkIcon }, + container: { + flexDirection: 'row', + backgroundColor: semantics.chatBgOutgoing, + padding: primitives.spacingXs, + borderRadius: components.messageBubbleRadiusAttachment, + ...linkPreviewList.container, + }, + imageWrapper: { + flexDirection: 'row', + overflow: 'hidden', + ...linkPreviewList.imageWrapper, + }, + dismissWrapper: { + position: 'absolute', + right: 0, + top: 0, + ...linkPreviewList.dismissWrapper, + }, + thumbnail: { + borderRadius: components.messageBubbleRadiusAttachment, + height: 40, + width: 40, + ...linkPreviewList.thumbnail, + }, + wrapper: { + paddingVertical: primitives.spacingXxs, + ...linkPreviewList.wrapper, + }, + metadataContainer: { + marginLeft: primitives.spacingXs, + flex: 1, + minWidth: 0, + ...linkPreviewList.metadataContainer, + }, + text: { + fontSize: primitives.typographyFontSizeXs, + // TODO: Change this to a better semantic once chatTextOutgoing is available + color: semantics.brand900, + ...linkPreviewList.text, + }, + titleText: { + fontWeight: primitives.typographyFontWeightBold, + fontSize: primitives.typographyFontSizeXs, + // TODO: Change this to a better semantic once chatTextOutgoing is available + color: semantics.brand900, + ...linkPreviewList.titleText, + }, + }), + [linkPreviewList, semantics.brand900, semantics.chatBgOutgoing], + ); +}; diff --git a/package/src/components/MessageInput/hooks/useLinkPreviews.ts b/package/src/components/MessageInput/hooks/useLinkPreviews.ts new file mode 100644 index 000000000..4f4e7beb8 --- /dev/null +++ b/package/src/components/MessageInput/hooks/useLinkPreviews.ts @@ -0,0 +1,27 @@ +import { LinkPreviewsManager, LinkPreviewsManagerState } from 'stream-chat'; + +import { useMessageComposer } from '../../../contexts'; +import { useStateStore } from '../../../hooks'; + +const linkPreviewsManagerStateSelector = (state: LinkPreviewsManagerState) => ({ + linkPreviews: Array.from(state.previews.values()).filter((preview) => + LinkPreviewsManager.previewIsLoaded(preview), + ), +}); + +export const useLinkPreviews = () => { + const messageComposer = useMessageComposer(); + const { linkPreviewsManager } = messageComposer; + const { linkPreviews } = useStateStore( + linkPreviewsManager.state, + linkPreviewsManagerStateSelector, + ); + + return linkPreviews; +}; + +export const useHasLinkPreviews = () => { + const linkPreviews = useLinkPreviews(); + + return linkPreviews.length > 0; +}; diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index 71f326a96..44aebeaea 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -1895,7 +1895,6 @@ exports[`Thread should match thread snapshot 1`] = ` } >