Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
40 changes: 26 additions & 14 deletions package/src/components/MessageInput/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -411,22 +417,21 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
{Input ? (
<Input additionalTextInputProps={additionalTextInputProps} getUsers={getUsers} />
) : (
<Animated.View
style={[styles.container, container]}
layout={LinearTransition.duration(200)}
>
<View style={[styles.container, container]}>
{isRecordingStateIdle ? (
<View
<Animated.View
layout={LinearTransition.duration(200)}
style={[
styles.inputButtonsContainer,
messageInputFloating ? styles.shadow : null,
inputButtonsContainer,
]}
>
{InputButtons && <InputButtons />}
</View>
</Animated.View>
) : null}
<View
<Animated.View
layout={LinearTransition.duration(200)}
style={[
styles.inputBoxWrapper,
messageInputFloating ? [styles.shadow, inputFloatingContainer] : null,
Expand All @@ -441,12 +446,15 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
<AudioRecordingInProgress />
) : null}
{isRecordingStateIdle ? (
<View
<Animated.View
layout={LinearTransition.duration(200)}
style={[
styles.contentContainer,
{
paddingTop:
hasAttachments || quotedMessage || editing ? primitives.spacingXs : 0,
hasAttachments || quotedMessage || editing || hasLinkPreviews
? primitives.spacingXs
: 0,
},
contentContainer,
]}
Expand All @@ -472,10 +480,14 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
</Animated.View>
) : null}
<AttachmentUploadPreviewList />
</View>
<LinkPreviewList />
</Animated.View>
) : null}

<View style={[styles.inputContainer, inputContainer]}>
<Animated.View
style={[styles.inputContainer, inputContainer]}
layout={LinearTransition.duration(200)}
>
{!isRecordingStateIdle ? (
<AudioRecorder slideToCancelStyle={slideToCancelAnimatedStyle} />
) : (
Expand All @@ -499,10 +511,10 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
<OutputButtons micPositionX={micPositionX} micPositionY={micPositionY} />
</View>
) : null}
</View>
</Animated.View>
</View>
</View>
</Animated.View>
</Animated.View>
</View>
)}
<ShowThreadMessageInChannelButton threadList={threadList} />
</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 @@ -1895,7 +1895,6 @@ exports[`Thread should match thread snapshot 1`] = `
}
>
<View
layout={BaseAnimationMock {}}
style={
[
{
Expand All @@ -1909,6 +1908,7 @@ exports[`Thread should match thread snapshot 1`] = `
}
>
<View
layout={BaseAnimationMock {}}
style={
[
{
Expand Down Expand Up @@ -2068,6 +2068,7 @@ exports[`Thread should match thread snapshot 1`] = `
</View>
</View>
<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
Loading