Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/SampleApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ const App = () => {
drafts: {
enabled: true,
},
linkPreviews: {
enabled: true,
}
});

setupCommandUIMiddlewares(composer);
Expand Down
8 changes: 4 additions & 4 deletions examples/SampleApp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
27 changes: 20 additions & 7 deletions package/src/components/MessageInput/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ 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 { useAudioRecorder } from './hooks/useAudioRecorder';
import { useCountdown } from './hooks/useCountdown';

import { useHasLinkPreviews } from './hooks/useLinkPreviews';

import {
ChatContextValue,
useAttachmentManagerState,
Expand Down Expand Up @@ -262,6 +265,9 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);

const { height } = useStateStore(messageInputHeightStore.store, messageInputHeightStoreSelector);

const hasLinkPreviews = useHasLinkPreviews();

const {
theme: {
semantics,
Expand Down Expand Up @@ -530,15 +536,16 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
/>
) : (
<>
<View
<Animated.View
layout={LinearTransition.duration(200)}
style={[
styles.inputButtonsContainer,
messageInputFloating ? styles.shadow : null,
inputButtonsContainer,
]}
>
{InputButtons && <InputButtons />}
</View>
</Animated.View>
<Animated.View
layout={LinearTransition.duration(200)}
style={[
Expand All @@ -553,11 +560,13 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
]}
>
<View style={[styles.inputBoxContainer, inputBoxContainer]}>
<View
<Animated.View
layout={LinearTransition.duration(200)}
style={[
styles.contentContainer,
{
paddingTop: hasAttachments || quotedMessage || editing ? 8 : 0,
paddingTop:
hasAttachments || quotedMessage || editing || hasLinkPreviews ? 8 : 0,
},
contentContainer,
]}
Expand All @@ -583,9 +592,13 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
</Animated.View>
) : null}
<AttachmentUploadPreviewList />
</View>
<LinkPreviewList />
</Animated.View>

<View style={[styles.inputContainer, inputContainer]}>
<Animated.View
layout={LinearTransition.duration(200)}
style={[styles.inputContainer, inputContainer]}
>
{command ? (
<CommandInput disabled={!isOnline} />
) : (
Expand All @@ -599,7 +612,7 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
<View style={[styles.outputButtonsContainer, outputButtonsContainer]}>
<OutputButtons />
</View>
</View>
</Animated.View>
</View>
</Animated.View>
</>
Expand Down
162 changes: 162 additions & 0 deletions package/src/components/MessageInput/components/LinkPreviewList.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<LinkPreviewCard key={linkPreview.og_scrape_url} linkPreview={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 (
<Animated.View layout={LinearTransition.duration(200)} style={styles.wrapper}>
<View style={styles.container}>
<View style={styles.imageWrapper}>
<ImageComponent source={{ uri: image_url ?? thumb_url }} style={styles.thumbnail} />
</View>
<View style={styles.metadataContainer}>
{title ? (
<Text numberOfLines={1} ellipsizeMode='tail' style={styles.titleText}>
{title}
</Text>
) : null}
{text ? (
<Text numberOfLines={1} ellipsizeMode='tail' style={styles.text}>
{text}
</Text>
) : null}
{og_scrape_url ? (
<View style={styles.linkContainer}>
<NewLink
height={styles.text.fontSize}
stroke={styles.text.color}
width={styles.text.fontSize}
style={styles.linkIcon}
/>
<Text numberOfLines={1} ellipsizeMode='tail' style={styles.text}>
{og_scrape_url}
</Text>
</View>
) : null}
</View>
</View>
<View style={styles.dismissWrapper}>
<AttachmentRemoveControl onPress={dismissPreview} />
</View>
</Animated.View>
);
};

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],
);
};
27 changes: 27 additions & 0 deletions package/src/components/MessageInput/hooks/useLinkPreviews.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,7 @@ exports[`Thread should match thread snapshot 1`] = `
}
>
<View
layout={BaseAnimationMock {}}
style={
[
{
Expand Down Expand Up @@ -2095,6 +2096,7 @@ exports[`Thread should match thread snapshot 1`] = `
}
>
<View
layout={BaseAnimationMock {}}
style={
[
{
Expand All @@ -2110,6 +2112,7 @@ exports[`Thread should match thread snapshot 1`] = `
}
/>
<View
layout={BaseAnimationMock {}}
style={
[
{
Expand Down
24 changes: 24 additions & 0 deletions package/src/contexts/themeContext/utils/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,18 @@ export type Theme = {
upload: ImageStyle;
};
wrapper: ViewStyle;
linkPreviewList: {
linkContainer: ViewStyle;
linkIcon: ViewStyle;
container: ViewStyle;
imageWrapper: ViewStyle;
dismissWrapper: ViewStyle;
thumbnail: ImageStyle;
wrapper: ViewStyle;
metadataContainer: ViewStyle;
text: TextStyle;
titleText: TextStyle;
};
};
messageList: {
container: ViewStyle;
Expand Down Expand Up @@ -1227,6 +1239,18 @@ export const defaultTheme: Theme = {
upload: {},
},
wrapper: {},
linkPreviewList: {
linkContainer: {},
linkIcon: {},
container: {},
imageWrapper: {},
dismissWrapper: {},
thumbnail: {},
wrapper: {},
metadataContainer: {},
text: {},
titleText: {},
},
},
messageList: {
container: {},
Expand Down
8 changes: 4 additions & 4 deletions package/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8357,10 +8357,10 @@ stdin-discarder@^0.2.2:
resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be"
integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==

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"
Expand Down
Loading