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(), thensetOverlay("gallery"). Don't renderImageGalleryyourself;OverlayProvideralready does. - Put the
WithComponentsthat replaces a gallery part aroundOverlayProvider. 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,giphyVersionandnumberOfImageGalleryGridColumnsonOverlayProvider. The store readsgiphyVersiononce, whenOverlayProvidermounts.
How it opens and closes
A component opens the gallery in two steps:
imageGalleryStateStore.openImageGallery({ messages, selectedAttachmentUrl, requesterNode })gives the store the messages to show and the attachment to start on.setOverlay("gallery")fromuseOverlayContext()makesOverlayProviderrenderImageGallery.
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
Open the gallery from your own thumbnails
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
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,
};Related
- OverlayProvider: the props that configure the gallery.
- OverlayContext:
overlay,setOverlayandoverlayOpacity. - Attachments and uploads: changing what a tap on a thumbnail does.
- Channel details lists:
MediaList, which opens the gallery on the channel's media.