diff --git a/src/components/Message/__tests__/Message.test.tsx b/src/components/Message/__tests__/Message.test.tsx
index 688d349dc..9d35a2a8c 100644
--- a/src/components/Message/__tests__/Message.test.tsx
+++ b/src/components/Message/__tests__/Message.test.tsx
@@ -227,7 +227,10 @@ describe(' component', () => {
});
await context.handleReaction(reaction.type);
- expect(sendReaction).toHaveBeenCalledWith(message.id, { type: reaction.type });
+ expect(sendReaction).toHaveBeenCalledWith(message.id, {
+ emoji_code: '❤️',
+ type: reaction.type,
+ });
});
it('should not send reaction without permission', async () => {
diff --git a/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx b/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx
index cc5780480..6a67a418a 100644
--- a/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx
+++ b/src/components/Message/hooks/__tests__/useReactionHandler.test.tsx
@@ -7,6 +7,8 @@ import { reactionHandlerWarning, useReactionHandler } from '../useReactionHandle
import { ChannelActionProvider } from '../../../../context/ChannelActionContext';
import { ChannelStateProvider } from '../../../../context/ChannelStateContext';
import { ChatProvider } from '../../../../context/ChatContext';
+import { ComponentProvider } from '../../../../context/ComponentContext';
+import { emojiToUnicode } from '../../../Reactions/reactionOptions';
import {
generateChannel,
generateMessage,
@@ -30,12 +32,14 @@ async function renderUseReactionHandlerHook(
params: {
channelContextProps?: Record;
channelStateContextOverrides?: Record;
+ componentContext?: Record;
message?: LocalMessage | null;
} = {},
) {
const {
channelContextProps = {},
channelStateContextOverrides = {},
+ componentContext = {},
message = generateMessage(),
} = params;
@@ -58,7 +62,7 @@ async function renderUseReactionHandlerHook(
})}
>
- {children}
+ {children}
@@ -103,13 +107,56 @@ describe('useReactionHandler custom hook', () => {
expect(deleteReaction).toHaveBeenCalledWith(message.id, reaction.type);
});
- it('should send reaction', async () => {
- const reaction = generateReaction({ user: bob });
+ it('should send reaction with emoji_code derived from the default reaction options', async () => {
const message = generateMessage({ own_reactions: [] });
const handleReaction = await renderUseReactionHandlerHook({ message });
- await handleReaction(reaction.type);
+ await handleReaction('love');
expect(sendReaction).toHaveBeenCalledWith(message.id, {
- type: reaction.type,
+ emoji_code: '❤️',
+ type: 'love',
+ });
+ });
+
+ it('should send reaction without emoji_code when the type has no unicode', async () => {
+ const message = generateMessage({ own_reactions: [] });
+ const handleReaction = await renderUseReactionHandlerHook({ message });
+ await handleReaction('unsupported-reaction-type');
+ expect(sendReaction).toHaveBeenCalledWith(message.id, {
+ type: 'unsupported-reaction-type',
+ });
+ });
+
+ it('should derive emoji_code from custom reaction options provided via context', async () => {
+ const message = generateMessage({ own_reactions: [] });
+ const handleReaction = await renderUseReactionHandlerHook({
+ componentContext: {
+ reactionOptions: {
+ quick: {
+ rocket: {
+ Component: () => null,
+ name: 'Rocket',
+ unicode: emojiToUnicode('🚀'),
+ },
+ },
+ },
+ },
+ message,
+ });
+ await handleReaction('rocket');
+ expect(sendReaction).toHaveBeenCalledWith(message.id, {
+ emoji_code: '🚀',
+ type: 'rocket',
+ });
+ });
+
+ it('should stamp emoji_code on the optimistic reaction preview', async () => {
+ const message = generateMessage({ own_reactions: [] });
+ const handleReaction = await renderUseReactionHandlerHook({ message });
+ await handleReaction('love');
+ const optimisticMessage = updateMessage.mock.calls[0][0];
+ expect(optimisticMessage.latest_reactions[0]).toMatchObject({
+ emoji_code: '❤️',
+ type: 'love',
});
});
diff --git a/src/components/Message/hooks/useReactionHandler.ts b/src/components/Message/hooks/useReactionHandler.ts
index 495325449..87f9a52ad 100644
--- a/src/components/Message/hooks/useReactionHandler.ts
+++ b/src/components/Message/hooks/useReactionHandler.ts
@@ -6,6 +6,11 @@ import { useThreadContext } from '../../Threads';
import { useChannelActionContext } from '../../../context/ChannelActionContext';
import { useChannelStateContext } from '../../../context/ChannelStateContext';
import { useChatContext } from '../../../context/ChatContext';
+import { useComponentContext } from '../../../context/ComponentContext';
+import {
+ defaultReactionOptions,
+ getEmojiCodeByReactionType,
+} from '../../Reactions/reactionOptions';
import type { LocalMessage, Reaction, ReactionResponse } from 'stream-chat';
@@ -17,6 +22,8 @@ export const useReactionHandler = (message?: LocalMessage) => {
const { updateMessage } = useChannelActionContext('useReactionHandler');
const { channel, channelCapabilities } = useChannelStateContext('useReactionHandler');
const { client } = useChatContext('useReactionHandler');
+ const { reactionOptions = defaultReactionOptions } =
+ useComponentContext('useReactionHandler');
const createMessagePreview = useCallback(
(add: boolean, reaction: ReactionResponse, message: LocalMessage): LocalMessage => {
@@ -69,18 +76,22 @@ export const useReactionHandler = (message?: LocalMessage) => {
[client.user, client.userID],
);
- const createReactionPreview = (type: string) => ({
+ const createReactionPreview = (type: string, emojiCode?: string) => ({
message_id: message?.id,
score: 1,
type,
user: client.user,
user_id: client.user?.id,
+ ...(emojiCode && { emoji_code: emojiCode }),
});
const toggleReaction = throttle(async (id: string, type: string, add: boolean) => {
if (!message || !channelCapabilities['send-reaction']) return;
- const newReaction = createReactionPreview(type) as ReactionResponse;
+ // Native emoji (e.g. "👍") for this reaction type, sent as `emoji_code` so
+ // push notifications in mobile SDKs can render the emoji.
+ const emojiCode = getEmojiCodeByReactionType(reactionOptions, type);
+ const newReaction = createReactionPreview(type, emojiCode) as ReactionResponse;
const tempMessage = createMessagePreview(add, newReaction, message);
try {
@@ -88,7 +99,10 @@ export const useReactionHandler = (message?: LocalMessage) => {
thread?.upsertReplyLocally({ message: tempMessage });
const messageResponse = add
- ? await channel.sendReaction(id, { type } as Reaction)
+ ? await channel.sendReaction(id, {
+ type,
+ ...(emojiCode && { emoji_code: emojiCode }),
+ } as Reaction)
: await channel.deleteReaction(id, type);
// seems useless as we're expecting WS event to come in and replace this anyway
diff --git a/src/components/Reactions/__tests__/reactionOptions.test.ts b/src/components/Reactions/__tests__/reactionOptions.test.ts
new file mode 100644
index 000000000..c764ed4ff
--- /dev/null
+++ b/src/components/Reactions/__tests__/reactionOptions.test.ts
@@ -0,0 +1,59 @@
+import {
+ defaultReactionOptions,
+ emojiToUnicode,
+ getEmojiCodeByReactionType,
+ mapEmojiMartData,
+} from '../reactionOptions';
+
+const noop = () => null;
+
+describe('getEmojiCodeByReactionType', () => {
+ it('returns the native emoji for a quick reaction type', () => {
+ expect(getEmojiCodeByReactionType(defaultReactionOptions, 'like')).toBe('👍');
+ expect(getEmojiCodeByReactionType(defaultReactionOptions, 'love')).toBe('❤️');
+ expect(getEmojiCodeByReactionType(defaultReactionOptions, 'haha')).toBe('😂');
+ });
+
+ it('returns the native emoji for an extended reaction type', () => {
+ const reactionOptions = {
+ extended: {
+ rocket: { Component: noop, name: 'Rocket', unicode: emojiToUnicode('🚀') },
+ },
+ quick: {},
+ };
+
+ expect(getEmojiCodeByReactionType(reactionOptions, 'rocket')).toBe('🚀');
+ });
+
+ it('returns undefined for an unknown reaction type', () => {
+ expect(getEmojiCodeByReactionType(defaultReactionOptions, 'does-not-exist')).toBe(
+ undefined,
+ );
+ });
+
+ it('returns undefined when the matched option has no unicode', () => {
+ const reactionOptions = { quick: { custom: { Component: noop, name: 'Custom' } } };
+
+ expect(getEmojiCodeByReactionType(reactionOptions, 'custom')).toBe(undefined);
+ });
+
+ it('returns undefined for legacy array reaction options (no unicode data)', () => {
+ const reactionOptions = [{ Component: noop, name: 'Like', type: 'like' }];
+
+ expect(getEmojiCodeByReactionType(reactionOptions, 'like')).toBe(undefined);
+ });
+});
+
+describe('mapEmojiMartData', () => {
+ it('stores the unicode code point on each mapped entry', () => {
+ const mapped = mapEmojiMartData({
+ emojis: {
+ joy: { name: 'Joy', skins: [{ native: '😂' }] },
+ },
+ });
+
+ const unicode = emojiToUnicode('😂');
+ expect(mapped[unicode].unicode).toBe(unicode);
+ expect(mapped[unicode].name).toBe('Joy');
+ });
+});
diff --git a/src/components/Reactions/reactionOptions.tsx b/src/components/Reactions/reactionOptions.tsx
index bc9cffdea..97b3718a1 100644
--- a/src/components/Reactions/reactionOptions.tsx
+++ b/src/components/Reactions/reactionOptions.tsx
@@ -48,6 +48,7 @@ export const mapEmojiMartData = (
newMap[unicode] = {
Component: () => <>{nativeEmoji}>,
name: emojiData.name,
+ unicode,
};
}
@@ -111,3 +112,25 @@ export const getHasExtendedReactions = (reactionOptions: ReactionOptions) =>
!Array.isArray(reactionOptions) &&
typeof reactionOptions.extended !== 'undefined' &&
Object.keys(reactionOptions.extended).length > 0;
+
+/**
+ * Resolves the native emoji character (e.g. "👍") for a given reaction type from
+ * the configured reaction options. The value is used as the `emoji_code` sent
+ * with a reaction so that push notifications can render the emoji.
+ *
+ * Returns `undefined` when no `unicode` is available for the type (e.g. legacy
+ * array reaction options or custom options that omit `unicode`).
+ */
+export const getEmojiCodeByReactionType = (
+ reactionOptions: ReactionOptions,
+ reactionType: string,
+): string | undefined => {
+ // Legacy array reaction options carry no unicode data.
+ if (Array.isArray(reactionOptions)) return undefined;
+
+ const unicode =
+ reactionOptions.quick[reactionType]?.unicode ??
+ reactionOptions.extended?.[reactionType]?.unicode;
+
+ return unicode ? unicodeToEmoji(unicode) : undefined;
+};