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`] = `
}
>