// not exported directly (hidden, leading to poor DX)
import { defaultMinimalEmojis } from 'stream-chat-react/dist/components/Channel/emojiData';
export const customReactions = [
{
// relies on EmojiMart-supplied sprite sheet, "native" option does not work for Stream-supplied reactions
// you'd need to look up supported id's in stream-emoji.json under "emojis" key
id: 'bulb',
},
// unsupported
{
id: 'rick_roll',
},
// this wouldn't work
...defaultMinimalEmojis,
];
Upgrade to v11
Introducing new reactions
When it came to developer experience regarding customization of the reaction components our team noticed that our integrators generally struggled to make more advanced adjustments to reactions without having to rebuild the whole component set. The whole process has been quite unintuitive and that’s why this update aims at making adjusting your reactions much easier.
Main reasons for a revamp
- inability to reuse default (Stream supplied reactions) with your custom ones
- strong reliance on
emoji-mart
and inability to use completely custom reactions out of the box - certain
additionalEmojiProps
did not work with Stream-supplied reactions
New default setup and how it works
SDK by default comes with five pre-defined reaction types (haha
, like
, love
, sad
and wow
) which are newly rendered by StreamEmoji component which utilises sprite sheet system and renders images for each reaction to make sure it works flawlessly on every system. Default reaction options are defined in defaultReactionOptions and these options are reused for both ReactionSelector
and ReactionsList
(as well as SimpleReactionsList
). These options come by default from the ComponentContext but local component property will be prioritised if defined. This is how it works under the hood:
contextReactionOptions = defaultReactionOptions;
// ...
const reactionOptions = propsReactionOptions ?? contextReactionOptions;
Beware that sixth reaction type angry
has been removed in this update, if you need to re-enable it, follow this guide.
Custom reaction types and components
The default StreamEmoji component is rendered with the fixed size. You can change the size of the rendered emoji using --str-chat__stream-emoji-size
CSS variable:
<div style={{ '--str-chat__stream-emoji-size': '2em' }}>
<StreamEmoji fallback='😂' type='haha' />
</div>
It’s possible to supply your own reaction types and components to represent such reactions - let’s implement reaction of type rick_roll
to render a Rick Roll GIF and define override for default type love
:
import { Channel } from 'stream-chat-react';
const RickRollReaction = () => (
<img src='https://media.tenor.com/x8v1oNUOmg4AAAAM/rickroll-roll.gif' style={{ height: 20 }} />
);
const customReactionOptions = [
{
Component: RickRollReaction,
type: 'rick_roll',
name: 'Rick Roll',
},
{
Component: () => <>❤️</>,
type: 'love',
name: 'Heart',
},
];
And then you can pass these newly created options to Channel
component which will be then propagated to ReactionSelector
and ReactionsList
:
<Channel reactionOptions={customReactionOptions}>{/*...*/}</Channel>
Emoji Mart integration
If you’re used to work with EmojiMart emojis then integrating with new reaction system shouldn’t be a big trouble as you can define how your components look and reach for context if you need to:
// arbitrary EmojiMartContext (does not come with the SDK)
import { useEmojiMartContext } from '../contexts';
const CustomLikeComponent = () => {
const { selectedSkinTones, selectedSet } = useEmojiMartContext();
// to use EmojiMart web components you'll need to go through initiation steps, see EmojiMart documentation
return <em-emoji id='+1' set={selectedSet} skin={selectedSkinTones['+1']} />;
};
const reactionOptions = [
{
type: 'like',
component: CustomLikeComponent,
name: 'EmojiMart like',
},
];
// pass reaction options to component context (Channel) or to ReactionSelector and ReactionsList
Use of different reaction components for the same reaction types for ReactionSelector
and ReactionsList
components
If you need more fine-grain tuning and want to - for example - enable only a certain subset of clickable reactions but want to display the full set then you’d do something like this:
const JoyReaction = () => <>😂</>;
const CatReaction = () => <>🐈</>;
const ThumbsUpReaction = () => <>👍</>;
const SmileReaction = () => <>🙂</>;
// subset of clickable options available to the user
const selectedReactionOptions = [
{ type: 'haha', Component: JoyReaction },
{ type: 'cat', Component: CatReaction },
];
// set of all available options to be rendered
const completeReactionOptions = [
{ type: 'haha', Component: JoyReaction },
{ type: 'cat', Component: CatReaction },
{ type: '+1', Component: ThumbsUpReaction },
{ type: 'smile', Component: SmileReaction },
];
Or if you just want bigger icons for ReactionsList
while ReactionSelector
uses regular:
// arbitrary import (does not come with the SDK)
import { ReactionComponent } from './CustomReactions';
const selectorReactionOptions = [
{
type: 'like',
component: ReactionComponent.Like,
name: 'Like',
},
];
const listReactionOptions = [
{
type: 'like',
// in this example it's just different size of the emoji
component: ReactionComponent.LikeMedium,
name: 'Like medium',
},
];
You can then apply these new options to ReactionSelector
and ReactionsList
directly:
import { ReactionSelector, ReactionsList, Channel } from 'stream-chat-react';
// ReactionSelector component requires forwarded reference
const CustomReactionSelector = forwardRef((props, ref) => (
<ReactionSelector {...props} ref={ref} reactionOptions={selectorReactionOptions} />
));
const CustomReactionsList = (props) => (
<ReactionsList {...props} reactionOptions={listReactionOptions} />
);
And then pass them down to component context (Channel
) component:
<Channel ReactionSelector={CustomReactionSelector} ReactionsList={CustomReactionsList}>
{/*...*/}
</Channel>
Use of SpriteImage
component
We suggest using individual images per reaction type as multiple smaller requests is more bandwidth-friendly. Use this component only if you have no other choice.
If you have a sprite sheet of emojis and there’s no other way for you to implement your reactions, you can utilise SpriteImage utility component which comes with the SDK:
import { SpriteImage } from 'stream-chat-react';
const SPRITE_URL = 'https://getstream.imgix.net/images/emoji-sprite.png';
const reactionOptions = [
{
type: 'joy',
component: () => (
// renders fallback initially and then image when it successfully loads
<SpriteImage
columns={2} // number of spritesheet columns
rows={3} // number of spritesheet rows
spriteUrl={SPRITE_URL} // source URL of the spritesheet
position={[0, 1]} // x and y axis positions, zero indexed
fallback='😂' // native emoji (or any string) to render while the sprite image is loading
/>
),
name: 'ROFL',
},
];
// pass reaction options to component context (Channel) or to ReactionSelector and ReactionsList
Transition from previous version of custom reactions
Default setup
The transition is super easy:
import { defaultReactionOptions } from 'stream-chat-react';
// old custom options
const reactionOptions = [{ id: 'bulb' /* ...extra properties... */ }, { id: '+1' }, { id: 'joy' }];
// would newly become
const newReactionOptions = [
{ type: 'bulb', Component: () => <>💡</>, name: 'Bulb reaction' },
{ type: '+1', Component: () => <>👍</> },
{ type: 'joy', Component: () => <>🤣</>, name: 'ROFL' },
// reuse default ones if you want to
...defaultReactionOptions,
];
All of the extra options previously applied to EmojiMart
emojis can now be directly applied to your custom components either manually or through your custom context. For more information see EmojiMart integration.
Re-enabling angry
reaction
For better compatibility with other SDKs we decided to consolidate default available types and remove angry
type which was previously available only in the React SDK. Here’s how you’d go about re-enabling it:
import { StreamEmoji, defaultReactionOptions } from 'stream-chat-react';
const reactionOptions = [
...defaultReactionOptions,
{ type: 'angry', Component: () => <StreamEmoji fallback='😠' type='angry' />, name: 'Angry' },
];
// pass reaction options to component context (Channel) or to ReactionSelector and ReactionsList
Dropping support for built-in EmojiIndex
By default, our SDK comes bundled with the emoji-mart
’s emojiIndex
, more specifically - NimbleEmojiIndex
class which is then instantiated with custom emojiData
by our SDK. This index serves as a tool for efficiently searching through the emoji list and returning a subset that matches the search criteria (query). Within our SDK, this functionality is utilized by our autocomplete component, triggered by entering :<query>
to the meessage input. This functionality will continue to be integrated within our SDK. However, due to our decision to discontinue the use of emoji-mart
within the SDK, this feature will now be available on an opt-in basis. With the updated types and interface this will also allow integrators to supply their own emojiSearchIndex
instead of relying exclusively on the one supplied by emoji-mart
.
Reinstate emoji autocomplete behavior (search for emojis with :
)
Add emoji-mart
to your packages and make sure the package versions fit our peer-dependency requirements:
yarn add emoji-mart @emoji-mart/data
Import SearchIndex
and data
from emoji-mart
, initiate these data and then and pass SearchIndex
to our MessageInput
component:
import { MessageInput } from 'stream-chat-react';
import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data';
init({ data });
export const WrappedMessageInput = () => {
return <MessageInput emojiSearchIndex={SearchIndex} focus />;
};
Build your custom emojiSearchIndex
Prerequisities
Your data returned from the search
method should have at least these three properies which our SDK relies on:
- name - display name for the emoji, ie:
"Smile"
- id - unique emoji identificator
- skins - an array of emojis with different skins (our SDK uses only the first one in this array), ie:
[{ native: "😄" }]
Optional properties:
- emoticons - an array of strings to match substitutions with, ie:
[":D", ":-D", ":d"]
- native - native emoji string (old
emoji-mart
API), ie:"😄"
- will be prioritized if specified
Example
import { type EmojiSearchIndex } from 'stream-chat-react';
import search from '@jukben/emoji-search';
const emoticonMap: Record<string, string[]> = {
'😃': [':D', ':-D'],
'😑': ['-_-'],
'😢': [":'("],
};
const emojiSearchIndex: EmojiSearchIndex = {
search: (query) => {
const results = search(query);
return results.slice(0, 15).map((data) => ({
emoticons: emoticonMap[data.name],
id: data.name,
name: data.keywords.slice(1, data.keywords.length).join(', '),
native: data.name,
skins: [],
}));
},
};
export const WrappedChannel = ({ children }) => (
<Channel emojiSearchIndex={emojiSearchIndex}>{children}</Channel>
);
Migrate from v10
to v11
(EmojiIndex
becomes emojiSearchIndex
)
EmojiIndex
has previously lived in the EmojiContext
passed to through Channel
component. But since EmojiContext
no longer exists in our SDK, the property has been moved to our ComponentContext
(still passed through Channel
) and changed its name to emojiSearchIndex
to properly repesent its funtionality. If your custom EmojiIndex
worked with our default components in v10
then it should still work in v11
without any changes to its search
method output:
Your old code:
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex, customData } from './CustomEmojiIndex';
const App = () => {
return (
<Channel emojiData={customData} EmojiIndex={CustomEmojiIndex}>
{/* other components */}
<MessageInput />
</Channel>
);
};
Should newly look like this:
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex, customData } from './CustomEmojiIndex';
// instantiate the search index
const customEmojiSearchIndex = new CustomEmojiIndex(customData);
const App = () => {
return (
<Channel emojiSearchIndex={customEmojiSearchIndex}>
{/* other components */}
<MessageInput />
</Channel>
);
};
Or enable it in either of the MessageInput
components individually:
import { Channel, MessageInput } from 'stream-chat-react';
// arbitrary import
import { CustomEmojiIndex, customData } from './CustomEmojiIndex';
// instantiate the search index
const customEmojiSearchIndex = new CustomEmojiIndex(customData);
const App = () => {
return (
<Channel>
{/* other components */}
<MessageInput emojiSearchIndex={customEmojiSearchIndex} />
<Thread additionalMessageInputProps={{ emojiSearchIndex: customEmojiSearchIndex }} />
</Channel>
);
};
Dropping support for built-in EmojiPicker
By default - our SDK would ship with emoji-mart
dependency on top of which our EmojiPicker
component is built. And since the SDK is using emoji-mart
for this component - it was also reused for reactions (ReactionsList
and ReactionSelector
) and suggestion list (MessageInput
). This solution proved to be very uncomfortable to work with when it came to replacing either of the mentioned components (or disabling them completely) and the final applications using our SDK would still bundle certain emoji-mart
parts which weren’t needed (or seemingly “disabled”) resulting in sub-optimal load times. Maintaining such architecture became a burden so we’re switching things a bit.
Changes to the default component composition (architecture)
SDK’s EmojiPicker
component now comes as two-part “bundle” - a button and an actual picker element. The component now holds its own open
state which is handled by clicking the button (or anywhere else to close it).
Switching to opt-in mechanism (BREAKING CHANGE)
We made emoji-mart
package in our SDK completely optional which means that EmojiPicker
component is now disabled by default.
Reinstate the EmojiPicker
component
To reinstate the previous behavior you’ll have to add emoji-mart
to your packages and make sure the packages come with versions that fit our peer-dependency requirements:
yarn add emoji-mart @emoji-mart/data @emoji-mart/react
Import EmojiPicker
component from the stream-chat-react
package:
import { Channel } from 'stream-chat-react';
import { EmojiPicker } from 'stream-chat-react/emojis';
// and apply it to the Channel (component context)
const WrappedChannel = ({ children }) => {
return <Channel EmojiPicker={EmojiPicker}>{children}</Channel>;
};
Build your custom EmojiPicker
(with example)
If emoji-mart
is too heavy for your use-case and you’d like to build your own you can certainly do so, here’s a very simple EmojiPicker
example built using emoji-picker-react
package:
import EmojiPicker from 'emoji-picker-react';
import { useMessageInputContext } from 'stream-chat-react';
export const CustomEmojiPicker = () => {
const [open, setOpen] = useState(false);
const { insertText, textareaRef } = useMessageInputContext();
return (
<>
<button onClick={() => setOpen((isOpen) => !isOpen)}>Open EmojiPicker</button>
{open && (
<EmojiPicker
onEmojiClick={(emoji, event) => {
insertText(emoji.native);
textareaRef.current?.focus(); // returns focus back to the message input element
}}
/>
)}
</>
);
};
// and pass it down to the `Channel` component
You can make the component slightly better using FloatingUI
by wrapping the actual picker element to make it float perfectly positioned above the button. See the source of the EmojiPicker component which comes with the SDK for inspiration.
Old emoji-mart
(v3.0.1)
Even though it’s not explicitly provided by our SDK anymore, it’s still possible for our integrators to use older version of the emoji-mart
- specifically version 3.0.1
on top of which our old components were built. We don’t recommend using old version of the emoji-mart
but if you really need to, follow the 3.0.1
documentation in combination with the previous guide to build your own EmojiPicker
component with the old emoji-mart
API. Beware though, if you wish to use slightly modified emoji-mart
CSS previously supplied by our SDK by default in the main index.css
file, you’ll now have to explicitly import it:
import 'stream-chat-react/css/v2/index.css';
import 'stream-chat-react/css/v2/emoji-mart.css';
Channel instance as a first argument to doSendMessageRequest
The doSendMessageRequest
will from now on be passed the Channel
instance instead of its CID to avoid forcing the developers to recreate a reference to the Channel
instance inside the doSendMessageRequest
function. The developers should adjust their implementation of doSendMessageRequest
to call directly await channel.sendMessage(messageData, options)
:
import { ChannelProps } from 'stream-chat-react';
const doSendMessageRequest: ChannelProps['doSendMessageRequest'] = async (
channel,
messageData,
options,
) => {
// optional custom logic
await channel.sendMessage(messageData, options);
// optional custom logic
};
Message text rendering
Optional remark plugins htmlToTextPlugin
, keepLineBreaksPlugin
introduced with stream-chat-react@v10.19.0 are now included among the default remark plugins. That means that in the messages that originally contained a sequence of multiple newline characters \n
, these will be replaced with line break elements <br/>
. The number of line breaks equals count of newline characters minus 1.
The dependencies used to parse the markdown with renderText
function have been upgraded as a result of that, the following changes had to be performed in stream-chat-react library:
ReactMarkdownProps
dropped from customMarkDownRenderers
RenderTextOptions.customMarkDownRenderers
- a mapping of element name and corresponding React component to be rendered. The components are no longer accepting ReactMarkdownProps
User mention renderer props change
The RenderTextOptions.customMarkDownRenderers.mention
props have been reduced. From now on, only children
and node
are passed to the component. And so now mention
renderer props look as follows:
import { PropsWithChildren } from 'react';
import type { UserResponse } from 'stream-chat';
import type { DefaultStreamChatGenerics } from 'stream-chat-react';
type MentionProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = PropsWithChildren<{
node: {
mentionedUser: UserResponse<StreamChatGenerics>;
};
}>;
Adjust custom rehype or remark plugins
If you have implemented your own rehype or remark plugin using visit
function from the library unist-util-visit
beware that the index
and parent
arguments of the Visitor
function cannot be null
but undefined
instead. You should be notified by Typescript about this and should adjust the type checks accordingly.
If you would like to prevent applying plugins htmlToTextPlugin
, keepLineBreaksPlugin
, you can customize your renderText()
by overriding the remark plugins. The example below will keep the plugin remarkGfm
and exclude the rest:
import remarkGfm from 'remark-gfm';
import { renderText, RenderTextPluginConfigurator } from 'stream-chat-react';
const getRemarkPlugins: RenderTextPluginConfigurator = () => {
return [[remarkGfm, { singleTilde: false }]];
};
const customRenderText = (text, mentionedUsers) =>
renderText(text, mentionedUsers, {
getRemarkPlugins,
});
const CustomMessageList = () => <MessageList renderText={customRenderText} />;
Flag useImageFlagEmojisOnWindows
Since this release you’ll need to explicitly import extra stylesheet from stream-chat-react/css/v2/emoji-replacement.css
as it has been removed from our main stylesheet to reduce final bundle size for integrators who do not wish to use this feature.
import { Chat } from 'stream-chat-react';
import 'stream-chat-react/css/v2/index.css';
import 'stream-chat-react/css/v2/emoji-replacement.css';
export const WrappedChat = ({ children, client }) => (
<Chat useImageFlagEmojisOnWindows client={client}>
{children}
</Chat>
);
- Introducing new reactions
- Main reasons for a revamp
- New default setup and how it works
- Custom reaction types and components
- Emoji Mart integration
- Use of different reaction components for the same reaction types for ReactionSelector and ReactionsList components
- Use of SpriteImage component
- Transition from previous version of custom reactions
- Dropping support for built-in EmojiIndex
- Dropping support for built-in EmojiPicker
- Channel instance as a first argument to doSendMessageRequest
- Message text rendering
- Flag useImageFlagEmojisOnWindows