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

Image gallery

The image gallery is the full-screen viewer for the images, videos and GIFs of messages. OverlayProvider renders it, above your navigation, while its overlay is 'gallery', and reads what to show from an ImageGalleryStateStore in ImageGalleryContext. This page covers how it opens, its parts and their override keys, the context and the store. Overlays and bottom sheets shows it from the user's side.

Best practices

  • Open the gallery through the store and the overlay, as the SDK does: imageGalleryStateStore.openImageGallery(), then setOverlay("gallery"). Don't render ImageGallery yourself; OverlayProvider already does.
  • Put the WithComponents that replaces a gallery part around OverlayProvider. The gallery renders there, outside your screens.
  • Pass requesterNode, the native handle of the thumbnail that opened the gallery. Screen reader focus returns to it when the gallery closes.
  • A header of your own takes over clearing the store. The default header calls imageGalleryStateStore.clear() when the gallery closes, which also stops its videos; call it when yours unmounts.
  • Set autoPlayVideo, giphyVersion and numberOfImageGalleryGridColumns on OverlayProvider. The store reads giphyVersion once, when OverlayProvider mounts.

How it opens and closes

A component opens the gallery in two steps:

  1. imageGalleryStateStore.openImageGallery({ messages, selectedAttachmentUrl, requesterNode }) gives the store the messages to show and the attachment to start on.
  2. setOverlay("gallery") from useOverlayContext() makes OverlayProvider render ImageGallery.

The store keeps the messages that have an attachment it can show: an image that isn't a link preview, a Giphy attachment, or a video when a video player is installed. It turns every attachment of those messages into an asset, so a file sent in the same message as an image becomes a slide too. It starts on the asset whose URL matches selectedAttachmentUrl, ignoring the query string, or on the first one.

A message's gallery attachment opens it with that one message, so the user swipes through the message's images and videos. The media list of channel details opens it with every message it has loaded. Inside channel details, the list is in a native modal that covers OverlayProvider's layer, so the navigation section renders a gallery inside the modal too.

The back button of the header, a swipe down, and the Android back button close the gallery by setting the overlay to 'none'.

Parts

Part Component Override key
The viewer: the pager, zoom and the gestures ImageGallery None
The header: the back button, the sender and the time ImageGalleryHeader ImageGalleryHeader
The footer: the share button, the position and the grid button ImageGalleryFooter ImageGalleryFooter
The video controls, at the top of the footer for a video ImageGalleryVideoControl ImageGalleryVideoControls
The grid of every asset, in a bottom sheet ImageGalleryGrid ImageGalleryGrid

The header shows the sender's name, or their id, or "Unknown User", and the time the message was sent, formatted with the timestamp.ImageGalleryHeader translation key. The footer shows the position, such as "2 of 5". Its share button saves a remote asset to a file and opens the share sheet, and shows only when the share and file system handlers are registered. The video controls have play and pause, the position, a seek bar and a SpeedSettingsButton. Pressing an asset in the grid moves the gallery to it and closes the grid.

ImageGallery props

ImageGallery reads all of its props from context, and you don't render it in an app. They're listed for a custom overlay that renders it.

Prop Type Default Description
numberOfImageGalleryGridColumns number from ImageGalleryContext The columns of the grid. Without a value in context, 3.
overlayOpacity SharedValue<number> from OverlayContext The opacity the gestures fade while the gallery is dragged down.
ImageGalleryHeader React.ComponentType<ImageGalleryHeaderProps> the ImageGalleryHeader key The header.
ImageGalleryFooter React.ComponentType<ImageGalleryFooterProps> the ImageGalleryFooter key The footer.
ImageGalleryGrid React.ComponentType<ImageGalleryGridProps> the ImageGalleryGrid key The grid.

Part props

The gallery passes these props to the parts. A replacement reads the asset it shows from the store, as the defaults do.

ImageGalleryHeaderProps:

Prop Type Default Description
opacity SharedValue<number> required Fades from 1 to 0 while the user drags the image down.
visible SharedValue<number> required 1 while the header and footer show, 0 after a tap hides them, animated between.

ImageGalleryFooterProps:

Prop Type Default Description
accessibilityLabel string required The label of the footer. The gallery passes a fixed English label.
opacity SharedValue<number> required As for the header.
openGridView () => void required Opens the grid.
visible SharedValue<number> required As for the header.

The default footer renders the ImageGalleryVideoControls key when the current asset is a video.

ImageGalleryGridProps:

Prop Type Default Description
closeGridView () => void required Closes the grid.
numberOfImageGalleryGridColumns number The columns. The default grid uses 3 without one.

ImageGalleryVideoControlProps:

Prop Type Default Description
attachmentId string required The id of the current asset, which is also the id of its video player.

ImageGalleryContext

OverlayProvider provides it, through ImageGalleryProvider, from its own props of the same names. useImageGalleryContext() reads it, and throws outside OverlayProvider.

Value Type Description
imageGalleryStateStore ImageGalleryStateStore The store of the gallery. The provider creates it once and registers its subscriptions.
autoPlayVideo boolean Whether a video starts playing when the gallery shows it. false when unset.
giphyVersion keyof NonNullable<Attachment['giphy']> The Giphy rendition the gallery shows. The store uses 'fixed_height' when unset.
numberOfImageGalleryGridColumns number The columns of the grid.

ImageGalleryProvider takes them as value, of type ImageGalleryProviderProps, without the store.

ImageGalleryStateStore

The store holds what the gallery shows. Its state is a StateStore of ImageGalleryState; read it with useStateStore and a selector at module scope.

Field Type Description
messages LocalMessage[] The messages the gallery was opened with.
assets ImageGalleryAsset[] One asset per attachment of the messages with an attachment it can show, in message order. Recomputed when messages changes.
currentIndex number The index of the asset on screen.
selectedAttachmentUrl string The URL to start on, when set. Setting it moves currentIndex to its asset.
requesterNode number | null The native handle that gets screen reader focus back when the gallery closes.

An ImageGalleryAsset has the asset's id, uri, type, mime_type, thumb_url, original_width and original_height, and its message's messageId, channelId, created_at, user and user_id.

Member What it does
openImageGallery({ messages, selectedAttachmentUrl?, requesterNode? }) Sets the messages, the URL to start on and the requester in one update.
appendMessages(messages), removeMessages(messages) Adds or removes messages while the gallery is open.
currentIndex A setter that moves the gallery to an asset, and pauses the video of the asset it leaves.
clear() Empties the state and removes every video player.
videoPlayerPool The video players of the gallery, one per video asset, keyed by the asset's id.

useImageGalleryVideoPlayer({ id }) returns the player of an asset from the pool, creating it when it's missing, with autoPlayVideo from the context. The default video controls read its state for isPlaying, progress, duration and currentPlaybackRate, and call toggle(), seek() and changePlaybackRate().

Theming

The theme's imageGallery key styles the gallery: backgroundColor, pager and slide for the viewer, and header, footer, grid and videoControl for the parts. See Theme object.

Examples

src/MessageThumbnails.tsx
import { useRef } from "react";
import type { ComponentRef } from "react";
import { findNodeHandle, Image, Pressable, View } from "react-native";
import type { LocalMessage } from "stream-chat";
import {
  useImageGalleryContext,
  useOverlayContext,
} from "stream-chat-react-native";

export const MessageThumbnails = ({ message }: { message: LocalMessage }) => {
  const { imageGalleryStateStore } = useImageGalleryContext();
  const { setOverlay } = useOverlayContext();
  const refs = useRef<Record<string, ComponentRef<typeof View> | null>>({});

  const images = (message.attachments ?? []).filter(
    (attachment) => attachment.type === "image" && attachment.image_url,
  );

  return (
    <View style={{ flexDirection: "row", gap: 4 }}>
      {images.map((attachment) => {
        const url = attachment.image_url as string;
        return (
          <Pressable
            accessibilityLabel="Open image"
            accessibilityRole="button"
            key={url}
            onPress={() => {
              imageGalleryStateStore.openImageGallery({
                messages: [message],
                requesterNode: findNodeHandle(refs.current[url]),
                selectedAttachmentUrl: url,
              });
              setOverlay("gallery");
            }}
            ref={(view) => {
              refs.current[url] = view;
            }}
          >
            <Image source={{ uri: url }} style={{ height: 64, width: 64 }} />
          </Pressable>
        );
      })}
    </View>
  );
};

A header of your own

src/GalleryHeader.tsx
import { useEffect } from "react";
import { Pressable, Text, View } from "react-native";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
import {
  useImageGalleryContext,
  useOverlayContext,
  useStateStore,
} from "stream-chat-react-native";
import type {
  ComponentOverrides,
  ImageGalleryHeaderProps,
  ImageGalleryState,
} from "stream-chat-react-native";

const selector = ({ assets, currentIndex }: ImageGalleryState) => ({
  senderName: assets[currentIndex]?.user?.name,
});

const GalleryHeader = ({ opacity }: ImageGalleryHeaderProps) => {
  const { imageGalleryStateStore } = useImageGalleryContext();
  const { setOverlay } = useOverlayContext();
  const { senderName } = useStateStore(imageGalleryStateStore.state, selector);
  const style = useAnimatedStyle(() => ({ opacity: opacity.value }));

  // The default header clears the store when the gallery closes; this one has to as well.
  useEffect(
    () => () => imageGalleryStateStore.clear(),
    [imageGalleryStateStore],
  );

  return (
    <Animated.View style={[{ paddingTop: 48 }, style]}>
      <View
        style={{
          flexDirection: "row",
          justifyContent: "space-between",
          padding: 12,
        }}
      >
        <Pressable
          accessibilityRole="button"
          onPress={() => setOverlay("none")}
        >
          <Text>Close</Text>
        </Pressable>
        <Text>{senderName ?? ""}</Text>
      </View>
    </Animated.View>
  );
};

// Pass these to a WithComponents around OverlayProvider.
export const galleryOverrides: ComponentOverrides = {
  ImageGalleryHeader: GalleryHeader,
};