Skip to main content
Version: v5

Custom Attachments

How to build a custom Attachment

Attachments is simply a property on message object:

const messageObject = {
id: '12312jh3b1jh2b312',
text: 'This is my test message!',
attachments: [
{
type: 'image',
thumb_url: '',
},
{
type: 'file',
asset_url: '',
},
],
};

Depending on value of message.attachments[index].type, we render attachments in different views. By default, we have following built-in views for rendering attachments:

  • attachment type image - Gallery (single or multiple images)
  • attachment type giphy - Giphy
  • attachment type file - FileAttachment
  • attachment with URL - Card
GalleryGiphyUrlPreviewFileAttachment

We have some default implementation for these type of attachments, to display them in MessageList (as shows in screenshots above). But you can override these components with your own implementation, as shown in following 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 assign custom type on attachment. Any custom attachment gets rendered in Card view. You can add your own implementation for Card component.

const CustomCardComponent = ({ type, ...otherProperties }) => {
console.log(type);
console.log(otherProperties);

return (/** Your custom UI */)
}

<Channel Card={CustomCardComponent} />

Let's try to build location sharing functionality, using custom attachment:

Location sharing example

Lets build an example of location sharing option in the app:

  • Show a "Share Location" button next to input box. Channel component accepts a prop InputButtons, to add some custom buttons next to input box.

  • When user presses this button, it should fetch the current location coordinates of user, and send a message on channel as following:

    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 our example, we are going to use react-native-geolocation library. Please check their setup instruction on their docs.

    NOTE If you are testing on iOS simulator, you will need to set some dummy coordinates, as mentioned here. Also don't forget to enable "location update" capability in background mode, from Xcode

  • On the receiver end, location type attachment should be rendered in map view, in message list. We are going to use google static maps API to render map in message. You can use other libraries as well such as react-native-maps

NOTE: Before you start using the Maps Static API, you need a project with a billing account and the Maps Static API enabled. To learn more, see Set up in Cloud Console

  • When user presses on location type attachment, it should take him to Google Maps application, with given 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>;

Did you find this page helpful?