# Link previews

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

A link preview is the card with a title, a description and an image that shows for a link in a message. This page covers how the composer loads a preview while the user types, what's sent with the message, and how the message list shows it.

## What users get

The user types "Look at `https://example.com/launch`". When they stop typing, a card appears above the input with the page's image, title and description, and the URL. They can dismiss it with its remove button. When they send, the message in the list shows a card for the link above the text, and tapping the card opens the page.

<table>
  <tr>
  <td align='center' width='50%'>

![A link preview card above the composer input with the page title and URL and a remove button on iOS](https://getstream.io/docs-assets/images/1b6b9d622c49.png)

  </td>
  <td align='center' width='50%'>

![A link preview card above the composer input with the page title and URL and a remove button on Android](https://getstream.io/docs-assets/images/20b17cea0634.png)

  </td>
  </tr>
</table>

## How it works across the layers

- **State.** The composer's `linkPreviewsManager` finds the links in the text, loads a preview for each, and keeps them in its store. Sending adds the loaded previews to the message as attachments.
- **UI.** In the composer, `MessageInputHeaderView` renders the previews above the input as `LinkPreviewCard`s. In the message list, `Attachment` renders each preview with `UrlPreview`, or with `URLPreviewCompact`.
- **Layout.** Not involved.

### From typing to sending

1. Each text change starts a wait of `linkPreviews.debounceURLEnrichmentMs`, 1500 ms by default. When the user stops typing for that long, `linkPreviews.findURLFn` finds the URLs in the text.
2. Each new URL gets a preview with status `loading`, and the manager asks the server for the page's data with `client.getOG({ url })`. The preview becomes `loaded`, or `failed` when the request fails. When the text has no URL left, all previews are cleared.
3. Dismissing a preview calls `LinkPreviewsManager.dismissPreview(url)`. Its status becomes `dismissed`, and `linkPreviews.onLinkPreviewDismissed` is called.
4. On send, the composition middleware adds every loaded preview to the message's attachments, and tells the server not to add previews of its own (`skip_enrich_url`). It also sets `skip_enrich_url` when the user dismissed a preview, but only when the message carries attachments. A text-only message whose preview was dismissed goes out without the flag, and the server adds the card.
5. If a preview is still loading when the user sends, no previews are sent, and the server adds them while it saves the message.

When the user lacks the `send-links` capability in the channel and the text contains a link, sending shows an alert, "Links are disabled", and nothing is sent. [Message composer](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-composer/#link-previews) lists the statuses.

### In the composer and the message list

The composer shows only loaded previews, and only the first of them. The others are still sent. The card shows the image, the title, the description and the URL, and its remove button dismisses the preview. A preview that is loading or failed shows nothing.

In the message list, an attachment with `og_scrape_url` or `title_link` is a link preview. `Attachment` renders it with `UrlPreview`: the image, with a play indicator when the link is a video, then the title, the description and the URL. With `urlPreviewType="compact"` on `Channel` or `Thread`, it renders `URLPreviewCompact` instead, a row with a small thumbnail. Tapping a card opens `og_scrape_url`, or the image's URL when there is none.

A message that also carries files, images or videos shows no link cards, and the link stays a link in the text.

## Turning it on or off

Link previews are on by default. Two sides decide:

- **The client:** `messageComposer.linkPreviews.enabled`, `true` by default. It decides whether the composer loads and shows previews.
- **The server:** the channel type's `url_enrichment` setting. It decides whether the server adds previews to messages. With it off, the composer doesn't load previews either.

To stop the composer from loading previews:

```ts
client.config.set({
  messageComposer: { linkPreviews: { enabled: false } },
});
```

The server still adds previews to sent messages while `url_enrichment` is on, so messages keep their cards. Only the composer stops showing them. To send messages without cards, turn `url_enrichment` off for the channel type. [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/#who-decides-what) lists every setting both sides decide.

## Customizing

### Change how links are found and loaded

```ts
import { StreamChat } from "stream-chat";
import type { LinkPreview } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

client.config.set({
  messageComposer: {
    linkPreviews: {
      // Only links to your own domain get a preview.
      findURLFn: (text: string) =>
        Array.from(
          text.matchAll(/https:\/\/example\.com\/\S*/g),
          ([url]) => url,
        ),
      onLinkPreviewDismissed: (linkPreview: LinkPreview) => {
        console.log(`Dismissed ${linkPreview.og_scrape_url}`);
      },
    },
  },
});
```

`debounceURLEnrichmentMs` is read only when a composer is built, so set it before the client creates composers, for example in the `options` you pass to `useCreateChatClient`.

### Replace the previews in the composer

The composer's preview list has no override key of its own. `MessageInputHeaderView` renders it, together with the quoted or edited message and the attachment previews, so a replacement renders those too. This header shows every loaded preview as a line with its title:

```tsx
import { Pressable, StyleSheet, Text, View } from "react-native";
import { LinkPreviewsManager } from "stream-chat";
import type {
  LinkPreviewsManagerState,
  MessageComposerState,
} from "stream-chat";
import {
  useComponentsContext,
  useMessageComposer,
  useMessageComposerAPIContext,
  useStateStore,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

const loadedPreviewsSelector = ({ previews }: LinkPreviewsManagerState) => ({
  previews: Array.from(previews.values()).filter(
    LinkPreviewsManager.previewIsLoaded,
  ),
});

const quotedMessageSelector = ({ quotedMessage }: MessageComposerState) => ({
  quotedMessage,
});

const LinkPreviewLines = () => {
  const { linkPreviewsManager } = useMessageComposer();
  const { previews } = useStateStore(
    linkPreviewsManager.state,
    loadedPreviewsSelector,
  );

  return (
    <>
      {previews.map((preview) => (
        <View key={preview.og_scrape_url} style={styles.line}>
          <Text numberOfLines={1} style={styles.title}>
            {preview.title ?? preview.og_scrape_url}
          </Text>
          <Pressable
            onPress={() =>
              linkPreviewsManager.dismissPreview(preview.og_scrape_url)
            }
          >
            <Text>Remove</Text>
          </Pressable>
        </View>
      ))}
    </>
  );
};

const ComposerHeader = () => {
  const { AttachmentUploadPreviewList, Reply } = useComponentsContext();
  const messageComposer = useMessageComposer();
  const { quotedMessage } = useStateStore(
    messageComposer.state,
    quotedMessageSelector,
  );
  const { clearEditingState } = useMessageComposerAPIContext();
  const { editedMessage } = messageComposer;

  return (
    <View style={styles.header}>
      {editedMessage ? (
        <Reply
          mode="edit"
          onDismiss={clearEditingState}
          quotedMessage={editedMessage}
        />
      ) : quotedMessage ? (
        <Reply
          mode="reply"
          onDismiss={() => messageComposer.setQuotedMessage(null)}
        />
      ) : null}
      <AttachmentUploadPreviewList />
      <LinkPreviewLines />
    </View>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = {
  MessageInputHeaderView: ComposerHeader,
};

const styles = StyleSheet.create({
  header: { gap: 4, paddingHorizontal: 8 },
  line: { alignItems: "center", flexDirection: "row", gap: 8 },
  title: { flex: 1 },
});
```

It works in channel, thread and edit composers alike, because `useMessageComposer()` returns the composer it's rendered in. The SDK's header also hides itself while a voice recording runs, and animates its parts in and out.

### Change the card in the message list

`UrlPreview` and `URLPreviewCompact` are override keys, and receive the `attachment`. This one renders the link as a line of text:

```tsx
import { Linking, Pressable, StyleSheet, Text } from "react-native";
import type {
  ComponentOverrides,
  URLPreviewProps,
} from "stream-chat-react-native";

const LinkLine = ({ attachment }: URLPreviewProps) => {
  const url = attachment.og_scrape_url ?? attachment.title_link;

  return (
    <Pressable onPress={() => url && Linking.openURL(url)}>
      <Text style={styles.link}>{attachment.title ?? url}</Text>
    </Pressable>
  );
};

// Pass to `WithComponents`, declared once at module scope.
export const overrides: ComponentOverrides = { UrlPreview: LinkLine };

const styles = StyleSheet.create({
  link: { padding: 8, textDecorationLine: "underline" },
});
```

The SDK's cards report a tap to `onPressMessage` on `Channel`, with the emitter `urlPreview` and the URL in `additionalInfo`, and open the page as their default. A card of your own that should keep that reads `onPress` from `useMessageContext()` and calls it with the same payload.

## Reference

- [Attachments](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message/attachments/): `URLPreview`, `URLPreviewCompact` and the `Attachment` component.
- [Composer UI components](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-composer/composer-ui-components/): `MessageInputHeaderView` and the composer's other parts.
- [MessageComposer API](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/messagecomposer-api/): `linkPreviewsManager`.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/configuration-tree/): every `messageComposer.linkPreviews` setting.
- [Channel](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/channel/channel/): `urlPreviewType`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/link-previews/](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/link-previews/).