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
// 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,
];

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>
);
© Getstream.io, Inc. All Rights Reserved.