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. When the user stops typing, messageComposer.linkPreviewsManager finds the links in the text and loads a preview for each from the server.
  2. Dismissing a preview with its ✕ keeps the link in the text, but the message goes out without that preview.
  3. On send, the loaded previews go out as the message's attachments. If one is still loading, none go out, and the server may add its own.

Loading previews in State › Link previews covers the wait, the statuses and the manager's calls, and Sending previews what goes out with the message.

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