Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- ⌨️ **Keyboard Handling** - Smart keyboard avoidance for all platforms
- 💬 **System Messages** - Display system notifications in chat
- ⚡ **Quick Replies** - Bot-style quick reply buttons
- 😀 **Emoji Reactions** - Long-press to react, with reaction pills and an optional full emoji browser
- ✍️ **Typing Indicator** - Show when users are typing
- ✅ **Message Status** - Tick indicators for sent/delivered/read states
- ⬇️ **Scroll to Bottom** - Quick navigation button
Expand Down Expand Up @@ -662,6 +663,77 @@ const [isScrolledUp, setIsScrolledUp] = useState(false)
/>
```

### Emoji Reactions

Long-press a message to open a quick emoji picker; selected reactions render as pills below the bubble and toggle on tap. The core ships a lightweight quick picker (built on `react-native-gesture-handler` and `react-native-reanimated`, no extra dependencies). A full emoji browser is optional and demonstrated in the example app via the `renderReactionPicker` override.

<p align="center">
<img width="200" src="https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/master/media/reactions-picker.png" />
&nbsp;&nbsp;
<img width="200" src="https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/master/media/reactions-pills.png" />
&nbsp;&nbsp;
<img width="200" src="https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/master/media/reactions-emoji-browser.png" />
</p>

Store reactions on each message as a `reactions` array, then enable the feature and handle the toggle. Reaction state is owned by you, so it works with any backend:

```tsx
interface IChatMessage extends IMessage {
reactions?: MessageReaction[] // { emoji: string, userIds: (string | number)[] }[]
}

const CURRENT_USER_ID = 1

const handleReactionPress = useCallback((message: IChatMessage, emoji: string) => {
setMessages(prev =>
prev.map(m => {
if (m._id !== message._id)
return m

const existing = (m.reactions ?? []).find(r => r.emoji === emoji)
if (!existing)
return { ...m, reactions: [...(m.reactions ?? []), { emoji, userIds: [CURRENT_USER_ID] }] }

const userIds = existing.userIds.includes(CURRENT_USER_ID)
? existing.userIds.filter(id => id !== CURRENT_USER_ID)
: [...existing.userIds, CURRENT_USER_ID]

return {
...m,
reactions: userIds.length === 0
? (m.reactions ?? []).filter(r => r.emoji !== emoji)
: (m.reactions ?? []).map(r => (r.emoji === emoji ? { ...r, userIds } : r)),
}
})
)
}, [])

<GiftedChat
messages={messages}
onSend={onSend}
user={{ _id: CURRENT_USER_ID }}
reactions={{
isEnabled: true,
onReactionPress: handleReactionPress,
// Optional: provide a richer picker (e.g. a full emoji browser).
// See example/components/chat-examples/ReactionsExample.tsx
// renderReactionPicker: props => <MyEmojiPicker {...props} />,
}}
/>
```

#### Reactions Props (Grouped)

The `reactions` prop accepts:

- **`isEnabled`** _(Bool)_ - Enable emoji reactions (default `false`)
- **`emojis`** _(String[])_ - Emojis shown in the quick picker (default `['👍', '❤️', '😂', '😮', '😢', '👎']`)
- **`onReactionPress`** _(Function)_ - `(message, emoji) => void` called when an emoji is selected or a pill is tapped. Toggle logic is left to you
- **`renderReactions`** _(Function)_ - Override the reactions-display component rendered below the bubble
- **`renderReactionPicker`** _(Function)_ - Override the picker shown on long-press (use for a full emoji browser)
- **`containerStyle`**, **`reactionStyle`**, **`reactionActiveStyle`**, **`reactionTextStyle`**, **`reactionCountStyle`** - Styles for the reaction pills
- **`pickerContainerStyle`**, **`pickerEmojiStyle`** - Styles for the quick picker

---

## 📱 Platform Notes
Expand Down
3 changes: 2 additions & 1 deletion example/app/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useThemeColor } from '@/hooks/use-theme-color'

type ChatExample = 'basic' | 'customized-rendering' | 'slack' | 'links' | 'reply' | 'day-animated'
type ChatExample = 'basic' | 'customized-rendering' | 'slack' | 'links' | 'reply' | 'day-animated' | 'reactions'

const examples: Array<{ id: ChatExample, title: string, description: string }> = [
{ id: 'basic', title: 'Basic Example', description: 'Basic chat with keyboard logging for testing' },
Expand All @@ -17,6 +17,7 @@ const examples: Array<{ id: ChatExample, title: string, description: string }> =
{ id: 'slack', title: 'Slack Style', description: 'Slack-like message styling' },
{ id: 'reply', title: 'Reply Example', description: 'Example demonstrating reply functionality' },
{ id: 'day-animated', title: 'Day Animated', description: 'Multi-day chat with Load earlier for testing the animated day header' },
{ id: 'reactions', title: 'Reactions', description: 'Long-press a message to react with emojis, with a full emoji browser' },
]

export default function ExploreScreen () {
Expand Down
4 changes: 4 additions & 0 deletions example/app/chat/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export default function ChatLayout () {
name='day-animated'
options={{ title: 'Day Animated' }}
/>
<Stack.Screen
name='reactions'
options={{ title: 'Reactions' }}
/>
</Stack>
)
}
Expand Down
3 changes: 3 additions & 0 deletions example/app/chat/reactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ReactionsExample from '@/components/chat-examples/ReactionsExample'

export default ReactionsExample
130 changes: 130 additions & 0 deletions example/components/chat-examples/ReactionsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useCallback, useMemo, useState } from 'react'
import { StyleSheet, View, useColorScheme } from 'react-native'
import { GiftedChat, IMessage, MessageReaction, ReactionPickerProps } from 'react-native-gifted-chat'

import messagesData from '../../example-expo/data/messages'
import { useKeyboardVerticalOffset } from '../../hooks/useKeyboardVerticalOffset'
import { getColorSchemeStyle } from '../../utils/styleUtils'
import { EmojiReactionPicker } from './reactions/EmojiReactionPicker'

export interface IChatMessage extends IMessage {
reactions?: MessageReaction[]
}

const CURRENT_USER_ID = 1

export default function ReactionsExample () {
const [messages, setMessages] = useState<IChatMessage[]>(messagesData)
const colorScheme = useColorScheme()
const keyboardVerticalOffset = useKeyboardVerticalOffset()
const isDark = colorScheme === 'dark'

const user = useMemo(() => ({
_id: CURRENT_USER_ID,
name: 'Developer',
}), [])

const onSend = useCallback((newMessages: IChatMessage[] = []) => {
setMessages(previousMessages =>
GiftedChat.append(previousMessages, newMessages)
)
}, [])

// Toggle the current user's reaction for the given emoji on a message.
const handleReactionPress = useCallback((message: IChatMessage, emoji: string) => {
setMessages(previousMessages =>
previousMessages.map(m => {
if (m._id !== message._id)
return m

const existing = (m.reactions ?? []).find(r => r.emoji === emoji)
if (existing) {
const newUserIds = existing.userIds.includes(CURRENT_USER_ID)
? existing.userIds.filter(id => id !== CURRENT_USER_ID)
: [...existing.userIds, CURRENT_USER_ID]

return {
...m,
reactions: newUserIds.length === 0
? (m.reactions ?? []).filter(r => r.emoji !== emoji)
: (m.reactions ?? []).map(r =>
r.emoji === emoji ? { ...r, userIds: newUserIds } : r
),
}
}

return {
...m,
reactions: [...(m.reactions ?? []), { emoji, userIds: [CURRENT_USER_ID] }],
}
})
)
}, [])

// Provide a richer picker (quick bar + full emoji browser) via the override.
const renderReactionPicker = useCallback(
(pickerProps: ReactionPickerProps<IChatMessage>) => (
<EmojiReactionPicker
{...pickerProps}
isFullPickerEnabled
mode={isDark ? 'dark' : 'light'}
fullPickerLang='en'
fullPickerColumnCount={6}
fullPickerTheme={{
light: {
searchbar: {
container: { backgroundColor: '#f3f4f6' },
textInput: { backgroundColor: '#ffffff', color: '#111827', fontSize: 15 },
},
toolbar: { container: { backgroundColor: '#ffffff' } },
},
dark: {
searchbar: {
container: { backgroundColor: '#1f2937' },
textInput: { backgroundColor: '#374151', color: '#f9fafb', fontSize: 15 },
},
toolbar: { container: { backgroundColor: '#111827' } },
},
}}
/>
),
[isDark]
)

return (
<View style={[styles.container, getColorSchemeStyle(styles, 'container', colorScheme)]}>
<GiftedChat<IChatMessage>
messages={messages}
onSend={onSend}
user={user}
messagesContainerStyle={getColorSchemeStyle(styles, 'messagesContainer', colorScheme)}
textInputProps={{
style: getColorSchemeStyle(styles, 'composer', colorScheme),
}}
keyboardAvoidingViewProps={{ keyboardVerticalOffset }}
reactions={{
isEnabled: true,
onReactionPress: handleReactionPress,
renderReactionPicker,
}}
/>
</View>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
container_dark: {
backgroundColor: '#000',
},
messagesContainer_dark: {
backgroundColor: '#000',
},
composer_dark: {
backgroundColor: '#1a1a1a',
color: '#fff',
},
})
Loading
Loading