Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Link previews

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. Hovering the card shows the full URL. They can dismiss it with its ✕. When they send, the message in the list shows a card for the link above the text.

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. LinkPreviewList shows the previews above the input, as LinkPreviewCards. In the message list, Attachment renders each preview with Card.
  • 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.

Message composer lists the statuses.

In the composer and the message list

LinkPreviewList shows the previews that are loading or loaded. It shows only the first one by default; the others are still sent. LinkPreviewCard shows the image, title, description and URL, and its ✕ dismisses the preview.

In the message list, an attachment with og_scrape_url or title_link is a link preview, and Attachment renders it with Card.

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:

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 lists every setting both sides decide.

Customizing

import type { LinkPreview } from "stream-chat";

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 config you pass to useCreateChatClient.

Show more than one preview

Wrap LinkPreviewList and pass displayLinkCount:

import {
  Channel,
  LinkPreviewList,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";

const ThreeLinkPreviews = () => <LinkPreviewList displayLinkCount={3} />;

const overrides = { LinkPreviewList: ThreeLinkPreviews };

export const ChannelWithThreePreviews = () => (
  <WithComponents overrides={overrides}>
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  </WithComponents>
);

Replace the preview list

A list of your own reads linkPreviewsManager from the composer and renders the previews it wants. LinkPreviewsManager.previewIsLoaded() and LinkPreviewsManager.previewIsLoading() tell the statuses apart:

import type { LinkPreviewsManagerState } from "stream-chat";
import { LinkPreviewsManager } from "stream-chat";
import {
  LinkPreviewCard,
  useMessageComposerController,
  useStateStore,
} from "stream-chat-react";

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

export const LoadedLinkPreviews = () => {
  const { linkPreviewsManager } = useMessageComposerController();
  const { previews } = useStateStore(
    linkPreviewsManager.state,
    loadedPreviewsSelector,
  );

  if (!previews.length) return null;

  return (
    <div className="loaded-link-previews">
      {previews.map((preview) => (
        <LinkPreviewCard key={preview.og_scrape_url} linkPreview={preview} />
      ))}
    </div>
  );
};

Register it with <WithComponents overrides={{ LinkPreviewList: LoadedLinkPreviews }}>. It works in channel, thread and edit composers alike, because useMessageComposerController() returns the composer it's rendered in.

Change the card in the message list

Pass a Card of your own to Attachment, and register the result as the Attachment override:

import type { AttachmentProps, CardProps } from "stream-chat-react";
import { Attachment } from "stream-chat-react";

const CompactCard = ({ og_scrape_url, title, title_link }: CardProps) => (
  <a className="compact-card" href={title_link ?? og_scrape_url}>
    {title ?? og_scrape_url}
  </a>
);

export const AttachmentWithCompactCards = (props: AttachmentProps) => (
  <Attachment {...props} Card={CompactCard} />
);

Reference