const messageObject = {
id: "12312jh3b1jh2b312",
text: "This is my test message!",
attachments: [
{
type: "image",
thumb_url: "",
},
{
type: "file",
asset_url: "",
},
],
};Custom Attachments
How to build a custom attachment
Attachments are a property on the message object:
Attachment rendering depends on message.attachments[index].type. Built-in views:
- attachment type
image- Gallery (single or multiple images) - attachment type
giphy- Giphy - attachment type
file- FileAttachment - attachment with URL - Card
Best Practices
- Keep custom attachment
typevalues unique and stable so renderers stay deterministic. - Pass custom attachment renderers via
Channelprops to avoid forking the message list. - Include enough metadata on the attachment object to render without extra network calls.
- Fall back to the default
Cardrenderer for unknown types to avoid blank messages. - Validate attachment payloads before sending to prevent malformed renders for other clients.
|
|
|
|
| Gallery | Giphy | UrlPreview | FileAttachment |
Default components handle these types in MessageList. You can override them, for example:
const CustomGiphy = ({ attachment, onPressIn }) => {
console.log(attachment.type);
console.log(attachment.actions)
console.log(attachment.image_url)
console.log(attachment.thumb_url)
console.log(attachment.title)
console.log(attachment.type)
return (/** Your custom UI */)
}
const CustomGallery = ({ images, onPressIn }) => {
console.log(images);
return (/** Your custom UI */)
}
const CustomFileAttachment = ({ attachment }) => {
console.log(attachment.mime_type);
console.log(attachment.title);
console.log(attachment.file_size);
console.log(attachment.actions);
return (/** Your custom UI */)
}
const CustomUrlPreview = () => {
console.log(text);
console.log(thumb_url);
console.log(title);
return (/** Your custom UI */)
}
// Provide these custom components to Channel, as props.
<Channel
Gallery={CustomGallery}
Giphy={CustomGiphy}
FileAttachment={CustomFileAttachment}
UrlPreview={CustomUrlPreview}
>You can also use a custom type. Custom attachments render in the Card view by default, which you can override.
const CustomCardComponent = ({ type, ...otherProperties }) => {
console.log(type);
console.log(otherProperties);
return (/** Your custom UI */)
}
<Channel Card={CustomCardComponent} />Next, build location sharing with a custom attachment:
Location sharing example
Let's build a location sharing example:

Add a "Share Location" button next to the input. Use the
InputButtonsprop onChannelto insert custom buttons.On press, fetch the user's location and send a message like:
const messageWithLocation = { text: "This is my location", attachments: [ { type: "location", latitude: "50.212312", longitude: "-71.212659", // You can add more custom properties if needed. }, ], };For this example, use react-native-geolocation. Follow their setup instructions.
NOTE If you test on the iOS simulator, set dummy coordinates as described here. Also enable "location update" in background mode in Xcode.
On the receiver end, render the
locationattachment as a map in the message list. This example uses the Google Static Maps API. You can also use react-native-maps.
NOTE: Before using the Maps Static API, you need a billing-enabled project with the API enabled. See Set up in Cloud Console.
- When the user taps the attachment, open Google Maps at the coordinates.
Here is the full implementation of share location example:
import React, { useEffect, useState } from "react";
import {
Image,
Linking,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { StreamChat } from "stream-chat";
import {
Channel,
Chat,
MessageInput,
MessageList,
OverlayProvider as ChatOverlayProvider,
useChannelContext,
} from "stream-chat-react-native";
import {
SafeAreaProvider,
SafeAreaView,
useSafeAreaInsets,
} from "react-native-safe-area-context";
import Geolocation from "@react-native-community/geolocation";
import Svg, { Path } from "react-native-svg";
// ============================================================
// Fill in following values
// ============================================================
const API_KEY = "";
const USER_ID = "";
const USER_TOKEN = "";
const CHANNEL_ID = "";
// Reference: https://developers.google.com/maps/documentation/maps-static/get-api-key
const MAPS_API_KEY = "";
const chatClient = StreamChat.getInstance(API_KEY);
const user = { id: USER_ID };
// We are going to `await` following two calls, before rendering any UI component.
// Please check the App component at bottom.
const connectUserPromise = chatClient.connectUser(user, USER_TOKEN);
const channel = chatClient.channel("messaging", CHANNEL_ID);
// Basic utilities required for location sharing
// Given the location coordinates, this function generates URL for google map,
// and opens this Url using Linking module of react-native.
// Please check documentation of `Linking` module from react-native, for details:
// https://reactnative.dev/docs/linking
//
// Generally this URL will be opened in google maps application.
// https://developers.google.com/maps/documentation/urls/get-started
const goToGoogleMaps = (lat, long) => {
const url = `https://www.google.com/maps/search/?api=1&query=${lat},${long}`;
Linking.canOpenURL(url).then((supported) => {
if (supported) {
Linking.openURL(url);
} else {
console.log(`Don't know how to open URI: ${url}`);
}
});
};
// Generates static map url for given location coordinates.
// For reference, please check - https://developers.google.com/maps/documentation/maps-static/overview
const prepareStaticMapUrl = (lat, long) => {
let baseURL = "https://maps.googleapis.com/maps/api/staticmap?";
let url = new URL(baseURL);
let params = url.searchParams;
params.append("center", `${lat},${long}`);
params.append("zoom", "15");
params.append("size", "600x300");
params.append("maptype", "roadmap");
params.append("key", MAPS_API_KEY);
params.append("markers", `color:red|${lat},${long}`);
return url.toString();
};
// Send your current location attachment, as message, on current channel.
const sendCurrentLocation = () => {
Geolocation.getCurrentPosition((info) => {
channel?.sendMessage({
text: "This is my location",
attachments: [
{
type: "location",
latitude: info.coords.latitude,
longitude: info.coords.longitude,
},
],
});
});
};
// UI Component for rendering `location` type attachment
const LocationCard = ({ type, latitude, longitude }) => {
if (type === "location") {
const mapApi = prepareStaticMapUrl(latitude, longitude);
console.log(mapApi);
return (
<TouchableOpacity onPress={() => goToGoogleMaps(latitude, longitude)}>
<Image source={{ uri: mapApi }} style={{ height: 200, width: 300 }} />
</TouchableOpacity>
);
}
};
// Icon for "Share Location" button, next to input box.
const ShareLocationIcon = (props) => (
<Svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
<Path
d="M12 12c-1.654 0-3-1.345-3-3 0-1.654 1.346-3 3-3s3 1.346 3 3c0 1.655-1.346 3-3 3zm0-4a1.001 1.001 0 101 1c0-.551-.449-1-1-1z"
fill="#000"
/>
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M12 22s7-5.455 7-12.727C19 5.636 16.667 2 12 2S5 5.636 5 9.273C5 16.545 12 22 12 22zm1.915-4.857C15.541 15.032 17 12.277 17 9.273c0-1.412-.456-2.75-1.27-3.7C14.953 4.664 13.763 4 12 4s-2.953.664-3.73 1.573C7.456 6.523 7 7.86 7 9.273c0 3.004 1.459 5.759 3.085 7.87.678.88 1.358 1.614 1.915 2.166a21.689 21.689 0 001.915-2.166zm-.683 3.281s0 .001 0 0z"
fill="#000"
/>
</Svg>
);
// UI component to add Share Location button next to input box.
const InputButtons = () => {
const { channel: currentChannel } = useChannelContext();
return (
<TouchableOpacity
onPress={() => sendCurrentLocation(currentChannel)}
style={{ marginRight: 10 }}
>
<ShareLocationIcon />
</TouchableOpacity>
);
};
const ChannelScreen = () => {
const { bottom } = useSafeAreaInsets();
return (
<ChatOverlayProvider bottomInset={bottom}>
<SafeAreaView>
<Chat client={chatClient}>
{/* Setting keyboardVerticalOffset as 0, since we don't have any header yet */}
<Channel
channel={channel}
keyboardVerticalOffset={0}
Card={LocationCard}
InputButtons={InputButtons}
>
<View style={StyleSheet.absoluteFill}>
<MessageList />
<MessageInput />
</View>
</Channel>
</Chat>
</SafeAreaView>
</ChatOverlayProvider>
);
};
export default function App() {
const [ready, setReady] = useState();
useEffect(() => {
const initChat = async () => {
await connectUserPromise;
await channel.watch();
setReady(true);
};
initChat();
}, []);
if (!ready) {
return null;
}
return (
<SafeAreaProvider>
<ChannelScreen channel={channel} />
</SafeAreaProvider>
);
}Handling Custom Properties On Attachment
Default UI components and context providers from the SDK are memoized for performance purpose, and will not trigger re-renders upon updates to custom properties on attachment.
Eg: Suppose we add a customField property to the attachment object and use it the UI in custom Card component.
<Channel
Card={(attachment) => {
return (
<View>
<Text>{attachment.customField}</Text>
</View>
);
}}
/>In this example, if you try to update customField on particular attachment from backend (or anywhere), you will not see it updated on UI until you refresh the chat.
The reason being, the default memoization logic only checks for fixed set of properties on attachment, and doesn't check for custom properties.
This can be solved by providing a function which checks for changes in custom properties which you may have been defined on attachment.
import { Channel } from "stream-chat-react-native";
const isAttachmentEqualHandler = (prevAttachment, nextAttachment) => {
const attachmentEqual =
prevAttachment.customField === nextAttachment.customField;
if (!attachmentEqual) return false;
return true;
};
<Channel
channel={channel}
isAttachmentEqual={isAttachmentEqualHandler}
Card={(attachment) => {
return (
<View>
<Text>{attachment.customField}</Text>
</View>
);
}}
>
{/* The underlying components */}
</Channel>;


