@kesha-antonov/react-native-chat 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -47
- package/lib/Actions.d.ts +5 -5
- package/lib/Actions.d.ts.map +1 -1
- package/lib/Actions.js +28 -39
- package/lib/Actions.js.map +1 -1
- package/lib/Avatar.d.ts.map +1 -1
- package/lib/Avatar.js +34 -32
- package/lib/Avatar.js.map +1 -1
- package/lib/Bubble/index.d.ts.map +1 -1
- package/lib/Bubble/index.js +98 -28
- package/lib/Bubble/index.js.map +1 -1
- package/lib/Bubble/styles.d.ts +28 -11
- package/lib/Bubble/styles.d.ts.map +1 -1
- package/lib/Bubble/styles.js +108 -80
- package/lib/Bubble/styles.js.map +1 -1
- package/lib/Bubble/types.d.ts +12 -7
- package/lib/Bubble/types.d.ts.map +1 -1
- package/lib/Chat/ChatKeyboardAvoidingView.d.ts +8 -3
- package/lib/Chat/ChatKeyboardAvoidingView.d.ts.map +1 -1
- package/lib/Chat/ChatKeyboardAvoidingView.js +3 -5
- package/lib/Chat/ChatKeyboardAvoidingView.js.map +1 -1
- package/lib/Chat/index.d.ts +1 -0
- package/lib/Chat/index.d.ts.map +1 -1
- package/lib/Chat/index.js +94 -28
- package/lib/Chat/index.js.map +1 -1
- package/lib/Chat/types.d.ts +66 -24
- package/lib/Chat/types.d.ts.map +1 -1
- package/lib/ChatAvatar.d.ts.map +1 -1
- package/lib/ChatAvatar.js +11 -17
- package/lib/ChatAvatar.js.map +1 -1
- package/lib/ChatContext.d.ts +31 -1
- package/lib/ChatContext.d.ts.map +1 -1
- package/lib/ChatContext.js +4 -0
- package/lib/ChatContext.js.map +1 -1
- package/lib/Color.d.ts.map +1 -1
- package/lib/Color.js +6 -2
- package/lib/Color.js.map +1 -1
- package/lib/Composer.d.ts +14 -1
- package/lib/Composer.d.ts.map +1 -1
- package/lib/Composer.js +68 -50
- package/lib/Composer.js.map +1 -1
- package/lib/Constant.d.ts +1 -0
- package/lib/Constant.d.ts.map +1 -1
- package/lib/Constant.js +1 -0
- package/lib/Constant.js.map +1 -1
- package/lib/Day/index.d.ts.map +1 -1
- package/lib/Day/index.js +7 -3
- package/lib/Day/index.js.map +1 -1
- package/lib/Day/styles.d.ts +6 -7
- package/lib/Day/styles.d.ts.map +1 -1
- package/lib/Day/styles.js +11 -13
- package/lib/Day/styles.js.map +1 -1
- package/lib/Icons.d.ts +24 -0
- package/lib/Icons.d.ts.map +1 -0
- package/lib/Icons.js +2 -0
- package/lib/Icons.js.map +1 -0
- package/lib/InputToolbar.d.ts +18 -5
- package/lib/InputToolbar.d.ts.map +1 -1
- package/lib/InputToolbar.js +246 -19
- package/lib/InputToolbar.js.map +1 -1
- package/lib/LoadEarlierMessages.d.ts.map +1 -1
- package/lib/LoadEarlierMessages.js +12 -7
- package/lib/LoadEarlierMessages.js.map +1 -1
- package/lib/Message/index.d.ts.map +1 -1
- package/lib/Message/index.js +21 -12
- package/lib/Message/index.js.map +1 -1
- package/lib/Message/styles.js +2 -2
- package/lib/Message/styles.js.map +1 -1
- package/lib/MessageAudio.d.ts +2 -1
- package/lib/MessageAudio.d.ts.map +1 -1
- package/lib/MessageAudio.js +121 -19
- package/lib/MessageAudio.js.map +1 -1
- package/lib/MessageLocation.d.ts +9 -0
- package/lib/MessageLocation.d.ts.map +1 -0
- package/lib/MessageLocation.js +85 -0
- package/lib/MessageLocation.js.map +1 -0
- package/lib/MessageText.d.ts +12 -1
- package/lib/MessageText.d.ts.map +1 -1
- package/lib/MessageText.js +38 -11
- package/lib/MessageText.js.map +1 -1
- package/lib/MessageVideo.d.ts +2 -1
- package/lib/MessageVideo.d.ts.map +1 -1
- package/lib/MessageVideo.js +156 -18
- package/lib/MessageVideo.js.map +1 -1
- package/lib/MessagesContainer/components/Item/index.d.ts +2 -1
- package/lib/MessagesContainer/components/Item/index.d.ts.map +1 -1
- package/lib/MessagesContainer/components/Item/index.js +23 -1
- package/lib/MessagesContainer/components/Item/index.js.map +1 -1
- package/lib/MessagesContainer/index.d.ts.map +1 -1
- package/lib/MessagesContainer/index.js +24 -11
- package/lib/MessagesContainer/index.js.map +1 -1
- package/lib/MessagesContainer/styles.d.ts +23 -3
- package/lib/MessagesContainer/styles.d.ts.map +1 -1
- package/lib/MessagesContainer/styles.js +25 -5
- package/lib/MessagesContainer/styles.js.map +1 -1
- package/lib/Models.d.ts +72 -0
- package/lib/Models.d.ts.map +1 -1
- package/lib/QuickReplies.d.ts +1 -1
- package/lib/QuickReplies.d.ts.map +1 -1
- package/lib/QuickReplies.js +13 -7
- package/lib/QuickReplies.js.map +1 -1
- package/lib/Reactions/MessageReactions.d.ts.map +1 -1
- package/lib/Reactions/MessageReactions.js +11 -10
- package/lib/Reactions/MessageReactions.js.map +1 -1
- package/lib/Reactions/ReactionPicker.d.ts.map +1 -1
- package/lib/Reactions/ReactionPicker.js +5 -3
- package/lib/Reactions/ReactionPicker.js.map +1 -1
- package/lib/Send.d.ts +2 -2
- package/lib/Send.d.ts.map +1 -1
- package/lib/Send.js +37 -37
- package/lib/Send.js.map +1 -1
- package/lib/SystemMessage.d.ts.map +1 -1
- package/lib/SystemMessage.js +8 -5
- package/lib/SystemMessage.js.map +1 -1
- package/lib/Theme.d.ts +159 -0
- package/lib/Theme.d.ts.map +1 -0
- package/lib/Theme.js +147 -0
- package/lib/Theme.js.map +1 -0
- package/lib/Time.d.ts.map +1 -1
- package/lib/Time.js +7 -5
- package/lib/Time.js.map +1 -1
- package/lib/TypingIndicator/index.d.ts.map +1 -1
- package/lib/TypingIndicator/index.js +4 -1
- package/lib/TypingIndicator/index.js.map +1 -1
- package/lib/TypingIndicator/styles.d.ts +2 -2
- package/lib/TypingIndicator/styles.d.ts.map +1 -1
- package/lib/TypingIndicator/styles.js +4 -5
- package/lib/TypingIndicator/styles.js.map +1 -1
- package/lib/components/AttachmentSheet.d.ts +28 -0
- package/lib/components/AttachmentSheet.d.ts.map +1 -0
- package/lib/components/AttachmentSheet.js +177 -0
- package/lib/components/AttachmentSheet.js.map +1 -0
- package/lib/components/BasicMarkdown.d.ts +12 -0
- package/lib/components/BasicMarkdown.d.ts.map +1 -0
- package/lib/components/BasicMarkdown.js +220 -0
- package/lib/components/BasicMarkdown.js.map +1 -0
- package/lib/components/ContextMenu.d.ts +25 -0
- package/lib/components/ContextMenu.d.ts.map +1 -0
- package/lib/components/ContextMenu.js +122 -0
- package/lib/components/ContextMenu.js.map +1 -0
- package/lib/components/Icon.d.ts +17 -0
- package/lib/components/Icon.d.ts.map +1 -0
- package/lib/components/Icon.js +19 -0
- package/lib/components/Icon.js.map +1 -0
- package/lib/components/LucideIcon.d.ts +10 -0
- package/lib/components/LucideIcon.d.ts.map +1 -0
- package/lib/components/LucideIcon.js +118 -0
- package/lib/components/LucideIcon.js.map +1 -0
- package/lib/components/MarkdownMessageText.d.ts +19 -0
- package/lib/components/MarkdownMessageText.d.ts.map +1 -0
- package/lib/components/MarkdownMessageText.js +35 -0
- package/lib/components/MarkdownMessageText.js.map +1 -0
- package/lib/components/MediaCard.d.ts +19 -0
- package/lib/components/MediaCard.d.ts.map +1 -0
- package/lib/components/MediaCard.js +102 -0
- package/lib/components/MediaCard.js.map +1 -0
- package/lib/components/MediaControls.d.ts +41 -0
- package/lib/components/MediaControls.d.ts.map +1 -0
- package/lib/components/MediaControls.js +524 -0
- package/lib/components/MediaControls.js.map +1 -0
- package/lib/components/MessageReply.d.ts.map +1 -1
- package/lib/components/MessageReply.js +11 -9
- package/lib/components/MessageReply.js.map +1 -1
- package/lib/components/ReplyPreview.d.ts +3 -1
- package/lib/components/ReplyPreview.d.ts.map +1 -1
- package/lib/components/ReplyPreview.js +37 -36
- package/lib/components/ReplyPreview.js.map +1 -1
- package/lib/components/SendIcon.d.ts +13 -0
- package/lib/components/SendIcon.d.ts.map +1 -0
- package/lib/components/SendIcon.js +37 -0
- package/lib/components/SendIcon.js.map +1 -0
- package/lib/components/Ticks.d.ts +25 -0
- package/lib/components/Ticks.d.ts.map +1 -0
- package/lib/components/Ticks.js +102 -0
- package/lib/components/Ticks.js.map +1 -0
- package/lib/components/VideoNoteOverlay.d.ts +25 -0
- package/lib/components/VideoNoteOverlay.d.ts.map +1 -0
- package/lib/components/VideoNoteOverlay.js +151 -0
- package/lib/components/VideoNoteOverlay.js.map +1 -0
- package/lib/components/VideoNoteRecorder.d.ts +19 -0
- package/lib/components/VideoNoteRecorder.d.ts.map +1 -0
- package/lib/components/VideoNoteRecorder.js +634 -0
- package/lib/components/VideoNoteRecorder.js.map +1 -0
- package/lib/components/VideoRecordButton.d.ts +29 -0
- package/lib/components/VideoRecordButton.d.ts.map +1 -0
- package/lib/components/VideoRecordButton.js +117 -0
- package/lib/components/VideoRecordButton.js.map +1 -0
- package/lib/components/VoiceMessageInput.d.ts +33 -0
- package/lib/components/VoiceMessageInput.d.ts.map +1 -0
- package/lib/components/VoiceMessageInput.js +403 -0
- package/lib/components/VoiceMessageInput.js.map +1 -0
- package/lib/components/WaveformPlayer.d.ts +19 -0
- package/lib/components/WaveformPlayer.d.ts.map +1 -0
- package/lib/components/WaveformPlayer.js +367 -0
- package/lib/components/WaveformPlayer.js.map +1 -0
- package/lib/components/mediaPalette.d.ts +23 -0
- package/lib/components/mediaPalette.d.ts.map +1 -0
- package/lib/components/mediaPalette.js +16 -0
- package/lib/components/mediaPalette.js.map +1 -0
- package/lib/components/mediaPlayback.d.ts +22 -0
- package/lib/components/mediaPlayback.d.ts.map +1 -0
- package/lib/components/mediaPlayback.js +34 -0
- package/lib/components/mediaPlayback.js.map +1 -0
- package/lib/dayjsLocales.d.ts +18 -0
- package/lib/dayjsLocales.d.ts.map +1 -0
- package/lib/dayjsLocales.js +36 -0
- package/lib/dayjsLocales.js.map +1 -0
- package/lib/hooks/useColorScheme.d.ts +9 -2
- package/lib/hooks/useColorScheme.d.ts.map +1 -1
- package/lib/hooks/useColorScheme.js +9 -3
- package/lib/hooks/useColorScheme.js.map +1 -1
- package/lib/hooks/useIcons.d.ts +4 -0
- package/lib/hooks/useIcons.d.ts.map +1 -0
- package/lib/hooks/useIcons.js +8 -0
- package/lib/hooks/useIcons.js.map +1 -0
- package/lib/hooks/useIsRTL.d.ts +3 -0
- package/lib/hooks/useIsRTL.d.ts.map +1 -0
- package/lib/hooks/useIsRTL.js +7 -0
- package/lib/hooks/useIsRTL.js.map +1 -0
- package/lib/hooks/useKeyboardVerticalOffset.d.ts +29 -8
- package/lib/hooks/useKeyboardVerticalOffset.d.ts.map +1 -1
- package/lib/hooks/useKeyboardVerticalOffset.js +41 -8
- package/lib/hooks/useKeyboardVerticalOffset.js.map +1 -1
- package/lib/hooks/useLabels.d.ts +4 -0
- package/lib/hooks/useLabels.d.ts.map +1 -0
- package/lib/hooks/useLabels.js +8 -0
- package/lib/hooks/useLabels.js.map +1 -0
- package/lib/hooks/useTheme.d.ts +21 -0
- package/lib/hooks/useTheme.d.ts.map +1 -0
- package/lib/hooks/useTheme.js +32 -0
- package/lib/hooks/useTheme.js.map +1 -0
- package/lib/i18n.d.ts +66 -0
- package/lib/i18n.d.ts.map +1 -0
- package/lib/i18n.js +53 -0
- package/lib/i18n.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -1
- package/lib/locales/ar.d.ts +3 -0
- package/lib/locales/ar.d.ts.map +1 -0
- package/lib/locales/ar.js +22 -0
- package/lib/locales/ar.js.map +1 -0
- package/lib/locales/de.d.ts +3 -0
- package/lib/locales/de.d.ts.map +1 -0
- package/lib/locales/de.js +22 -0
- package/lib/locales/de.js.map +1 -0
- package/lib/locales/es.d.ts +3 -0
- package/lib/locales/es.d.ts.map +1 -0
- package/lib/locales/es.js +22 -0
- package/lib/locales/es.js.map +1 -0
- package/lib/locales/fr.d.ts +3 -0
- package/lib/locales/fr.d.ts.map +1 -0
- package/lib/locales/fr.js +22 -0
- package/lib/locales/fr.js.map +1 -0
- package/lib/locales/hi.d.ts +3 -0
- package/lib/locales/hi.d.ts.map +1 -0
- package/lib/locales/hi.js +22 -0
- package/lib/locales/hi.js.map +1 -0
- package/lib/locales/id.d.ts +3 -0
- package/lib/locales/id.d.ts.map +1 -0
- package/lib/locales/id.js +22 -0
- package/lib/locales/id.js.map +1 -0
- package/lib/locales/index.d.ts +19 -0
- package/lib/locales/index.d.ts.map +1 -0
- package/lib/locales/index.js +39 -0
- package/lib/locales/index.js.map +1 -0
- package/lib/locales/it.d.ts +3 -0
- package/lib/locales/it.d.ts.map +1 -0
- package/lib/locales/it.js +22 -0
- package/lib/locales/it.js.map +1 -0
- package/lib/locales/ja.d.ts +3 -0
- package/lib/locales/ja.d.ts.map +1 -0
- package/lib/locales/ja.js +22 -0
- package/lib/locales/ja.js.map +1 -0
- package/lib/locales/ko.d.ts +3 -0
- package/lib/locales/ko.d.ts.map +1 -0
- package/lib/locales/ko.js +22 -0
- package/lib/locales/ko.js.map +1 -0
- package/lib/locales/nl.d.ts +3 -0
- package/lib/locales/nl.d.ts.map +1 -0
- package/lib/locales/nl.js +22 -0
- package/lib/locales/nl.js.map +1 -0
- package/lib/locales/pl.d.ts +3 -0
- package/lib/locales/pl.d.ts.map +1 -0
- package/lib/locales/pl.js +22 -0
- package/lib/locales/pl.js.map +1 -0
- package/lib/locales/pt.d.ts +3 -0
- package/lib/locales/pt.d.ts.map +1 -0
- package/lib/locales/pt.js +22 -0
- package/lib/locales/pt.js.map +1 -0
- package/lib/locales/ru.d.ts +3 -0
- package/lib/locales/ru.d.ts.map +1 -0
- package/lib/locales/ru.js +22 -0
- package/lib/locales/ru.js.map +1 -0
- package/lib/locales/tr.d.ts +3 -0
- package/lib/locales/tr.d.ts.map +1 -0
- package/lib/locales/tr.js +22 -0
- package/lib/locales/tr.js.map +1 -0
- package/lib/locales/zh.d.ts +3 -0
- package/lib/locales/zh.d.ts.map +1 -0
- package/lib/locales/zh.js +22 -0
- package/lib/locales/zh.js.map +1 -0
- package/lib/rtl.d.ts +18 -0
- package/lib/rtl.d.ts.map +1 -0
- package/lib/rtl.js +31 -0
- package/lib/rtl.js.map +1 -0
- package/lib/types.d.ts +2 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +99 -44
- package/lib/MessageReply.d.ts +0 -15
- package/lib/MessageReply.d.ts.map +0 -1
- package/lib/MessageReply.js +0 -102
- package/lib/MessageReply.js.map +0 -1
- package/lib/ReplyPreview.d.ts +0 -14
- package/lib/ReplyPreview.d.ts.map +0 -1
- package/lib/ReplyPreview.js +0 -94
- package/lib/ReplyPreview.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://www.npmjs.com/package/@kesha-antonov/react-native-chat"><img alt="npm version" src="https://badge.fury.io/js/@kesha-antonov%2Freact-native-chat.svg"/></a>
|
|
3
3
|
<a href="https://www.npmjs.com/package/@kesha-antonov/react-native-chat"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@kesha-antonov%2Freact-native-chat.svg"/></a>
|
|
4
|
+
<a href="https://npm-stat.com/charts.html?package=%40kesha-antonov%2Freact-native-chat&from=2015-01-01"><img alt="total npm downloads" src="https://img.shields.io/badge/total%20downloads-466-blue.svg"/></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@kesha-antonov/react-native-chat"><img alt="npm downloads (last 18 months)" src="https://img.shields.io/npm/dt/@kesha-antonov%2Freact-native-chat.svg?label=18-months%20downloads"/></a>
|
|
4
6
|
<a href="https://github.com/kesha-antonov/react-native-chat/actions/workflows/main.yml"><img src="https://github.com/kesha-antonov/react-native-chat/actions/workflows/main.yml/badge.svg" alt="build"></a>
|
|
5
7
|
<img src="https://img.shields.io/badge/platforms-iOS%20%7C%20Android%20%7C%20Web-lightgrey.svg" alt="platforms">
|
|
6
8
|
<img src="https://img.shields.io/badge/TypeScript-supported-blue.svg" alt="TypeScript">
|
|
@@ -13,19 +15,34 @@
|
|
|
13
15
|
The most complete chat UI for React Native & Web
|
|
14
16
|
</p>
|
|
15
17
|
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img width="300" src="https://raw.githubusercontent.com/kesha-antonov/react-native-chat/main/media/streaming-demo.gif" alt="Streaming AI reply rendering markdown token by token" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
16
22
|
<p align="center">
|
|
17
23
|
<a href="https://snack.expo.dev/@kesha-antonov/react-native-chat-playground" target="_blank">
|
|
18
24
|
<img src="https://img.shields.io/badge/▶️_Try_in_Browser-4630EB?style=for-the-badge&logo=expo&logoColor=white" alt="Try Chat on Expo Snack"/>
|
|
19
25
|
</a>
|
|
20
26
|
</p>
|
|
21
27
|
|
|
22
|
-
>
|
|
28
|
+
<p align="center">
|
|
29
|
+
<strong>Coming from <code>react-native-gifted-chat</code>?</strong> This is a drop-in replacement -
|
|
30
|
+
same <code>IMessage</code>, same props, a few renamed exports.
|
|
31
|
+
<br/>
|
|
32
|
+
<a href="docs/MIGRATION.md">Migrate in one command with the codemod →</a>
|
|
33
|
+
</p>
|
|
23
34
|
|
|
24
35
|
---
|
|
25
36
|
|
|
26
37
|
## ✨ Features
|
|
27
38
|
|
|
39
|
+
> Actively maintained, New Architecture ready, and built for the latest Reanimated/Gesture Handler. A modern, themeable, drop-in successor to `react-native-gifted-chat`. **[See what is new ▸](#-whats-new-vs-react-native-gifted-chat)**
|
|
40
|
+
|
|
41
|
+
- 🌗 **[Modern UI, Dark Mode & Theming](#-whats-new-vs-react-native-gifted-chat)** - A clean, modern default look with a full light/dark theme system, runtime theme switching, and every token overridable
|
|
28
42
|
- 🤖 **[Streaming (AI) Messages](#streaming-ai-messages)** - Token-by-token streamed replies with a typing cursor and stop control
|
|
43
|
+
- 🎥 **[Video & Audio Messages](#-whats-new-vs-react-native-gifted-chat)** - Real inline playback (optional `expo-video` / `expo-audio`) with a graceful tappable fallback - no more "implement it yourself"
|
|
44
|
+
- 🎙️ **[Voice & Video Recording](#-whats-new-vs-react-native-gifted-chat)** - Telegram-style hold-to-record voice notes and camera video messages (optional, opt-in)
|
|
45
|
+
- 📍 **[Location Messages](#-whats-new-vs-react-native-gifted-chat)** - Map card that opens the system maps app on tap
|
|
29
46
|
- 🎨 **[Fully Customizable](#-props-reference)** - Override any component with your own implementation
|
|
30
47
|
- 📎 **[Composer Actions](#actions--action-sheet)** - Attach photos, files, or trigger custom actions
|
|
31
48
|
- ↩️ **[Reply to Messages](#reply-to-messages)** - Swipe-to-reply with reply preview and message threading
|
|
@@ -55,6 +72,75 @@
|
|
|
55
72
|
|
|
56
73
|
---
|
|
57
74
|
|
|
75
|
+
## 🆕 What's new vs react-native-gifted-chat
|
|
76
|
+
|
|
77
|
+
This library is kept in sync with upstream [`react-native-gifted-chat`](https://github.com/FaridSafi/react-native-gifted-chat)'s latest `master`, so you **keep everything it already has** - New Architecture support, the animated sticky day header, Reanimated 3/4, Gesture Handler, TypeScript - and the same `IMessage` model and prop names. **Moving over is a package swap plus a rename** (see [Migrating](#-migrating-from-react-native-gifted-chat)).
|
|
78
|
+
|
|
79
|
+
On top of that, this fork adds the features people kept asking upstream for, plus a modern look out of the box:
|
|
80
|
+
|
|
81
|
+
| Added in this fork | react-native-gifted-chat | @kesha-antonov/react-native-chat |
|
|
82
|
+
| --- | :---: | :---: |
|
|
83
|
+
| Active maintenance | 💤 sporadic | ✅ active |
|
|
84
|
+
| Modern default UI (Telegram-inspired) | dated 2020 look | ✅ modern, fully overridable |
|
|
85
|
+
| Light/Dark **theme system** | per-component color props | ✅ `theme` / `darkTheme` tokens, runtime switch |
|
|
86
|
+
| Streaming (AI) messages | ❌ | ✅ token-by-token + typing cursor |
|
|
87
|
+
| Emoji reactions | ❌ | ✅ long-press picker + reaction pills |
|
|
88
|
+
| Swipe-to-reply + reply preview | ❌ | ✅ built in |
|
|
89
|
+
| **Video / audio messages** | "not implemented, render your own" | ✅ inline players + tappable fallback |
|
|
90
|
+
| **Voice notes** (hold-to-record + waveform) | ❌ | ✅ optional, Telegram-style |
|
|
91
|
+
| **Video messages** (round camera notes) | ❌ | ✅ optional, Telegram-style |
|
|
92
|
+
| **Location messages** | ❌ ignored | ✅ map card → opens system maps |
|
|
93
|
+
| Bubble tails + tighter message grouping | flat bubbles | ✅ |
|
|
94
|
+
|
|
95
|
+
Everything new is **non-breaking and opt-in**: keep passing the same props you do today and you simply get the modern look and the extra features for free. The media/recording features only activate when you install their optional peer deps and enable them - nobody is forced to add a single dependency.
|
|
96
|
+
|
|
97
|
+
### Theming in one line
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
// Modern defaults out of the box, or override any token (light + dark):
|
|
101
|
+
<Chat
|
|
102
|
+
theme={{ colors: { accent: '#3390EC', outgoingBubble: '#EFFEDE' } }}
|
|
103
|
+
darkTheme={{ colors: { background: '#0E1621' } }}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Voice, video and location
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
<Chat
|
|
112
|
+
audioRecording={{ isEnabled: true }} // hold the mic to record a voice note (needs expo-audio)
|
|
113
|
+
videoRecording={{ isEnabled: true }} // record a video message (needs expo-image-picker)
|
|
114
|
+
// location messages render automatically for any IMessage with a `location`
|
|
115
|
+
{...props}
|
|
116
|
+
/>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Optional inline media playback + recording:
|
|
121
|
+
npx expo install expo-video expo-audio expo-image-picker
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Custom icons (e.g. Lucide)
|
|
125
|
+
|
|
126
|
+
Built-in icons are the official [Lucide](https://lucide.dev) glyphs, rendered via the optional `react-native-svg` peer when it is installed, or drawn with `View`s (no dependency) otherwise. Override any of them via the `icons` prop - for example with [`lucide-react-native`](https://lucide.dev/) - and the built-in icon is used for anything you don't override:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { Send, Mic } from 'lucide-react-native'
|
|
130
|
+
|
|
131
|
+
<Chat
|
|
132
|
+
icons={{
|
|
133
|
+
send: ({ color, size }) => <Send color={color} size={size} />,
|
|
134
|
+
mic: ({ color, size }) => <Mic color={color} size={size} />,
|
|
135
|
+
}}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Overridable names: `send`, `mic`, `camera`, `play`, `pause`, `check`, `checkAll`, `clock`, `pin`, `plus`, `close`, `chevronLeft`, `chevronDown`, `emoji`, `paperclip`, `reply`, `pencil`, `lock`, `trash`.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
58
144
|
<h3 align="center">Support This Project</h3>
|
|
59
145
|
|
|
60
146
|
<p align="center">
|
|
@@ -70,6 +156,7 @@
|
|
|
70
156
|
## 📖 Table of Contents
|
|
71
157
|
|
|
72
158
|
- [Features](#-features)
|
|
159
|
+
- [What's new vs react-native-gifted-chat](#-whats-new-vs-react-native-gifted-chat)
|
|
73
160
|
- [Requirements](#-requirements)
|
|
74
161
|
- [Installation](#-installation)
|
|
75
162
|
- [Migrating from react-native-gifted-chat](#-migrating-from-react-native-gifted-chat)
|
|
@@ -77,6 +164,7 @@
|
|
|
77
164
|
- [Props Reference](#-props-reference)
|
|
78
165
|
- [Data Structure](#-data-structure)
|
|
79
166
|
- [Platform Notes](#-platform-notes)
|
|
167
|
+
- [Performance](#-performance)
|
|
80
168
|
- [Example App](#-example-app)
|
|
81
169
|
- [Troubleshooting](#-troubleshooting)
|
|
82
170
|
- [Contributing](#-contributing)
|
|
@@ -265,6 +353,10 @@ interface User {
|
|
|
265
353
|
- **`messageIdGenerator`** _(Function)_ - Generate an id for new messages. Defaults to a simple random string generator.
|
|
266
354
|
- **`locale`** _(String)_ - Locale to localize the dates. You need first to import the locale you need (ie. `require('dayjs/locale/de')` or `import 'dayjs/locale/fr'`)
|
|
267
355
|
- **`colorScheme`** _('light' | 'dark')_ - Force color scheme (light/dark mode). When set to `'light'` or `'dark'`, it overrides the system color scheme. When `undefined`, it uses the system color scheme. Default is `undefined`.
|
|
356
|
+
- **`theme`** _(Object)_ - Override the default light theme tokens (`colors` / `radii` / `spacing` / `typography` / `avatar` / `sendButton` / `composer` / `voice`). Deep-merged over `defaultLightTheme`; any subset is allowed. See [Theming & Dark Mode](#theming--dark-mode).
|
|
357
|
+
- **`darkTheme`** _(Object)_ - Same as `theme`, applied when the resolved color scheme is dark (deep-merged over `defaultDarkTheme`).
|
|
358
|
+
- **`icons`** _(Object)_ - Icon override registry. Supply a render function for any built-in icon to replace it (e.g. with `lucide-react-native`). See [Custom icons](#custom-icons-eg-lucide).
|
|
359
|
+
- **`labels`** _(Object)_ - Override any UI string. See [Localization (i18n)](#localization-i18n).
|
|
268
360
|
|
|
269
361
|
### Refs
|
|
270
362
|
|
|
@@ -277,6 +369,7 @@ interface User {
|
|
|
277
369
|
|
|
278
370
|
Only used when Chat mounts the provider itself. If your app already mounts a `KeyboardProvider` (the setup `react-native-keyboard-controller` recommends - once, at the root), Chat detects it and reuses it instead of nesting a second one, and this prop is ignored. Configure the provider where you mount it.
|
|
279
371
|
- **`disableKeyboardProvider`** _(Bool)_ - Skip the built-in `KeyboardProvider` entirely; default is `false`. You do **not** need this just because your app mounts its own provider - that case is detected and reused. Reach for it only to opt out completely, e.g. when the provider's edge-to-edge behavior causes layout shift or a header jump on Android/Expo.
|
|
372
|
+
- **`disableGestureHandlerRootView`** _(Bool)_ - Skip the `GestureHandlerRootView` Chat mounts around itself; default is `false`. Unlike `KeyboardProvider`, an existing one can't be auto-detected - `react-native-gesture-handler` doesn't expose that publicly - so set this yourself if your app (or a library it uses, e.g. a bottom sheet) already mounts one at the root. Nesting a second one changes the native view hierarchy around the composer, which has been observed to make a rare Fabric/Yoga layout assertion on iOS more likely, especially when Chat is mounted and unmounted while the keyboard is open ([#17](https://github.com/kesha-antonov/react-native-chat/issues/17)).
|
|
280
373
|
- **`keyboardAvoidingViewProps`** _(Object)_ - Props to be passed to the [`KeyboardAvoidingView`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view). See **keyboardVerticalOffset** below for proper keyboard handling.
|
|
281
374
|
- **`isAlignedTop`** _(`boolean | 'auto'`)_ - Where the bubbles sit **while the whole conversation fits on screen**; once it is taller than the list this has no effect. `false` (default) keeps the usual bottom-anchored chat, `true` pins the messages to the top, and `'auto'` pins them to the top while the keyboard is closed and re-anchors them to the bottom while it is open - so a short conversation starts under the header and moves above the keyboard when the composer is focused ([#2736](https://github.com/FaridSafi/react-native-gifted-chat/issues/2736)). Works with either `isInverted` setting; ignored when `isFlashListEnabled` is set, since FlashList positions its own items.
|
|
282
375
|
- **`isInverted`** _(Bool)_ - Reverses display order of `messages`; default is `true`
|
|
@@ -303,21 +396,25 @@ If you do, sanity-check it on device: a toolbar behind the keyboard means the va
|
|
|
303
396
|
- **`initialText`** _(String)_ - Initial text to display in the input field
|
|
304
397
|
- **`isSendButtonAlwaysVisible`** _(Bool)_ - Always show send button in input text composer; default `false`, show only when text input is not empty
|
|
305
398
|
- **`isTextOptional`** _(Bool)_ - Allow sending messages without text (useful for media-only messages); default `false`. Use with `isSendButtonAlwaysVisible` for media attachments.
|
|
399
|
+
- **`isMultiline`** _(Bool)_ - Whether the composer accepts multiple lines; default `true`. With `true` the return key inserts a newline and you send with the send button. Set `false` for a single-line composer whose **return key sends the message** (the keyboard's return key becomes "send" and stays open afterwards).
|
|
306
400
|
- **`renderInputToolbar`** _(Component | Function)_ - Custom message composer container
|
|
307
401
|
- **`renderComposer`** _(Component | Function)_ - Custom text input message composer
|
|
308
402
|
- **`renderSend`** _(Component | Function)_ - Custom send button; you can pass children to the original `Send` component quite easily, for example, to use a custom icon ([example](https://github.com/kesha-antonov/react-native-chat/pull/487))
|
|
309
403
|
- **`renderActions`** _(Component | Function)_ - Custom action button on the left of the message composer
|
|
310
404
|
- **`renderAccessory`** _(Component | Function)_ - Custom second line of actions below the message composer
|
|
405
|
+
- **`onPressEmoji`** _(Function)_ - Callback for the optional emoji button on the left of the composer field. When omitted, the emoji button is hidden.
|
|
406
|
+
- **`audioRecording`** _(Object)_ - Enable Telegram-style hold-to-record voice notes. `{ isEnabled, minDurationMs?, onError? }`. Requires the optional `expo-audio` peer (and `react-native-audio-api` for the playback waveform); the mic button is hidden when it is absent.
|
|
407
|
+
- **`videoRecording`** _(Object)_ - Enable record-and-send video messages. `{ isEnabled, maxDuration?, onError? }`. Uses `react-native-vision-camera` for round camera notes, falling back to `expo-image-picker`'s system camera.
|
|
311
408
|
- **`textInputProps`** _(Object)_ - props to be passed to the [`<TextInput>`](https://reactnative.dev/docs/textinput).
|
|
312
409
|
|
|
313
410
|
> **Composer height** - there are no height props. The composer starts one line tall and grows with its content. Constrain it through `textInputProps.style` (e.g. `{ maxHeight: 120 }`), which is applied after the measured height and wins.
|
|
314
411
|
|
|
315
412
|
### Actions & Action Sheet
|
|
316
413
|
|
|
317
|
-
- **`
|
|
318
|
-
- **`
|
|
319
|
-
- **`
|
|
320
|
-
- **`actionSheetOptionTintColor`** _(String)_ - Tint color for action sheet
|
|
414
|
+
- **`actions`** _(Array)_ - Action options for the composer "+" button. Array of `{ title, action }`; add `icon` (and optional `color`) to an action to render a Telegram-style attachment **grid** (tiles) instead of a list. Opens the built-in themed `AttachmentSheet` - no extra dependency.
|
|
415
|
+
- **`onPressActionButton`** _(Function)_ - Callback when the "+" button is pressed (if set, the built-in `AttachmentSheet` is not shown)
|
|
416
|
+
- **`actionSheet`** _(Function)_ - Escape hatch for a custom system action sheet. **The bundled `@expo/react-native-action-sheet` dependency was removed**, so `context.actionSheet()` defaults to a no-op; pass your own implementation (with `ActionSheetProvider` in your tree) if you relied on it.
|
|
417
|
+
- **`actionSheetOptionTintColor`** _(String)_ - Tint color for action labels in the attachment sheet
|
|
321
418
|
|
|
322
419
|
### Messages & Message Container
|
|
323
420
|
|
|
@@ -336,11 +433,13 @@ If you do, sanity-check it on device: a toolbar behind the keyboard means the va
|
|
|
336
433
|
- **`renderMessageImage`** _(Component | Function)_ - Custom message image
|
|
337
434
|
- **`renderMessageVideo`** _(Component | Function)_ - Custom message video
|
|
338
435
|
- **`renderMessageAudio`** _(Component | Function)_ - Custom message audio
|
|
436
|
+
- **`renderMessageLocation`** _(Component | Function)_ - Custom renderer for `IMessage.location`; defaults to a map card that opens the system maps app on tap
|
|
437
|
+
- **`messageActions`** _(Array | Function(`message`))_ - Telegram-style long-press context menu. Each item is `{ label, icon?, onPress, destructive? }`. See [Message actions](#message-actions-long-press-context-menu).
|
|
339
438
|
- **`renderCustomView`** _(Component | Function)_ - Custom view inside the bubble
|
|
340
439
|
- **`isCustomViewBottom`** _(Bool)_ - Determine whether renderCustomView is displayed before or after the text, image and video views; default is `false`
|
|
341
440
|
- **`onPressMessage`** _(Function(`context`, `message`))_ - Callback when a message bubble is pressed
|
|
342
441
|
- **`onLongPressMessage`** _(Function(`context`, `message`))_ - Callback when a message bubble is long-pressed; you can use this to show action sheets (e.g., copy, delete, reply)
|
|
343
|
-
- **`isMessageGestureEnabled`** _(Bool | Function(`message`))_ - Whether the bubble itself is part of the row's tap / long-press surface that `reactions`
|
|
442
|
+
- **`isMessageGestureEnabled`** _(Bool | Function(`message`))_ - Whether the bubble itself is part of the row's tap / long-press surface that `reactions` and `messageActions` rely on; default is `true`. Pass `false`, or a predicate, for messages that render natively interactive content - the row beside the bubble stays pressable either way. See [Interactive content inside bubbles](#interactive-content-inside-bubbles-video-players-maps).
|
|
344
443
|
- **`imageProps`** _(Object)_ - Extra props to be passed to the [`<Image>`](https://reactnative.dev/docs/image) component created by the default `renderMessageImage`
|
|
345
444
|
- **`imageStyle`** _(Object)_ - Custom style for message images
|
|
346
445
|
- **`videoProps`** _(Object)_ - Extra props to be passed to the video component created by the required `renderMessageVideo`
|
|
@@ -654,6 +753,12 @@ const [isScrolledUp, setIsScrolledUp] = useState(false)
|
|
|
654
753
|
|
|
655
754
|
Render AI assistant replies token-by-token. The library batches incoming chunks with `requestAnimationFrame` (one render per frame, only the streaming bubble re-renders) and shows a blinking caret while a message is still streaming.
|
|
656
755
|
|
|
756
|
+
<p align="center">
|
|
757
|
+
<img width="250" src="https://raw.githubusercontent.com/kesha-antonov/react-native-chat/main/media/ai-streaming-markdown.png" alt="AI reply streaming token-by-token and rendering as markdown, with a blinking caret and a Stop button in the composer" />
|
|
758
|
+
</p>
|
|
759
|
+
|
|
760
|
+
> The reply above streams in token-by-token (note the caret `▋`) and renders as **markdown** - bold, italics, lists, inline and fenced code - while the composer's send button turns into a **Stop** control mid-stream. See [Markdown rendering for AI replies](#markdown-rendering-for-ai-replies) to enable markdown.
|
|
761
|
+
|
|
657
762
|
- **`IMessage.streaming`** - flag a message as streaming (shows the caret)
|
|
658
763
|
- **`useStreamingMessages(...)`** - owns the message list, rAF-batches `push()`, and supports stop via `AbortController`. It returns `{ messages, setMessages, append, startStream, isStreaming, stop }`; `setMessages` is there for anything the hook does not cover, so you can patch a message with a plain `map`.
|
|
659
764
|
|
|
@@ -681,6 +786,33 @@ function Bot () {
|
|
|
681
786
|
|
|
682
787
|
See **[docs/STREAMING.md](./docs/STREAMING.md)** for the full hook API and a real Claude streaming adapter (via a backend proxy). A runnable demo lives in `example/components/chat-examples/AIBotExample.tsx`.
|
|
683
788
|
|
|
789
|
+
#### Markdown rendering for AI replies
|
|
790
|
+
|
|
791
|
+
AI/LLM replies are usually markdown (bold, lists, fenced code). Enable markdown with `messageTextProps={{ markdown: true }}` (streamed messages auto-render as markdown unless you pass `markdown={false}`):
|
|
792
|
+
|
|
793
|
+
```tsx
|
|
794
|
+
// Force markdown for every message:
|
|
795
|
+
<Chat messageTextProps={{ markdown: true }} {...props} />
|
|
796
|
+
|
|
797
|
+
// Streamed messages auto-render as markdown; disable with markdown: false.
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
There are two renderers and you get the best available one automatically:
|
|
801
|
+
|
|
802
|
+
- **Built-in, zero-dependency renderer** (default). Covers headings, bullet/ordered lists, blockquotes, fenced + inline code, bold/italic/strikethrough, and links. It handles streaming-incomplete markdown gracefully (a half-written `**bold` or an unclosed code fence renders as plain text until complete), so it's safe to feed token-by-token. Nothing to install. Exposed as `BasicMarkdown` if you want to use it directly.
|
|
803
|
+
|
|
804
|
+
- **[`react-native-streamdown`](https://github.com/software-mansion-labs/react-native-streamdown)** (optional upgrade). When installed it's used instead, for richer streaming-safe markdown (tables, partial-table handling, etc.). It is a native module with its own peers - install the full set:
|
|
805
|
+
|
|
806
|
+
```bash
|
|
807
|
+
npx expo install react-native-streamdown react-native-enriched-markdown remend katex
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
It also requires `react-native-worklets >= 0.8.3` (i.e. `react-native-reanimated >= 4.3`), and `react-native-enriched-markdown` is a native module, so a dev build / prebuild is required (it does not work in Expo Go). Pass through Streamdown's own theming/rules via `markdownProps`:
|
|
811
|
+
|
|
812
|
+
```tsx
|
|
813
|
+
<Chat messageTextProps={{ markdown: true, markdownProps: { /* ... */ } }} {...props} />
|
|
814
|
+
```
|
|
815
|
+
|
|
684
816
|
### Emoji Reactions
|
|
685
817
|
|
|
686
818
|
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.
|
|
@@ -777,9 +909,25 @@ Message text is automatically scanned for URLs, emails, and phone numbers; hasht
|
|
|
777
909
|
|
|
778
910
|
For full control, pass custom `matchers` (`{ type, pattern, getLinkUrl?, getLinkText?, renderLink?, onPress? }[]`) to add or override patterns. See the Links example in the [example app](#-example-app).
|
|
779
911
|
|
|
912
|
+
### Message actions (long-press context menu)
|
|
913
|
+
|
|
914
|
+
Long-press a message to open a floating, themed **context menu** (Telegram style) anchored to the bubble. Provide the actions via `messageActions` - an array, or a function of the message - each `{ label, icon?, onPress, destructive? }`. When reactions are enabled, a reactions row is shown on top of the menu automatically.
|
|
915
|
+
|
|
916
|
+
```tsx
|
|
917
|
+
import { setStringAsync } from 'expo-clipboard'
|
|
918
|
+
import { Copy, Trash2 } from 'lucide-react-native' // optional icons
|
|
919
|
+
|
|
920
|
+
<Chat
|
|
921
|
+
messageActions={message => [
|
|
922
|
+
{ label: 'Copy', icon: ({ color, size }) => <Copy color={color} size={size} />, onPress: () => setStringAsync(message.text) },
|
|
923
|
+
{ label: 'Delete', destructive: true, onPress: () => deleteMessage(message) },
|
|
924
|
+
]}
|
|
925
|
+
/>
|
|
926
|
+
```
|
|
927
|
+
|
|
780
928
|
### Interactive content inside bubbles (video players, maps)
|
|
781
929
|
|
|
782
|
-
When `reactions` are enabled, the long-press surface spans the **whole message row** - the bubble *and* the empty space beside it, the way Telegram behaves on Android. (A tap gesture is added on top only when `onPressMessage` is set.) Those recognizers do not cancel touches on native subviews, so a `react-native-video` / `expo-video` player rendered through `renderMessageVideo` keeps its native `controls` interactive.
|
|
930
|
+
When `reactions` or `messageActions` are enabled, the long-press surface spans the **whole message row** - the bubble *and* the empty space beside it, the way Telegram behaves on Android. (A tap gesture is added on top only when `onPressMessage` is set.) Those recognizers do not cancel touches on native subviews, so a `react-native-video` / `expo-video` player rendered through `renderMessageVideo` keeps its native `controls` interactive.
|
|
783
931
|
|
|
784
932
|
If a message must own every touch that lands on it, set `isMessageGestureEnabled` to `false` for it. The gesture surface then drops *behind* the bubble: the bubble's content takes its touches, and long-pressing the row next to the bubble still opens the picker - so reactions are never lost for that message.
|
|
785
933
|
|
|
@@ -792,64 +940,55 @@ If a message must own every touch that lands on it, set `isMessageGestureEnabled
|
|
|
792
940
|
/>
|
|
793
941
|
```
|
|
794
942
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
On long histories `FlatList` can log `VirtualizedList: You have a large list that is slow to update`. [FlashList](https://shopify.github.io/flash-list/) v2 recycles rows instead of keeping them mounted, which removes that class of stall. It is supported as an **optional** dependency - install it yourself and flip one prop:
|
|
798
|
-
|
|
799
|
-
```bash
|
|
800
|
-
yarn add @shopify/flash-list
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
```jsx
|
|
804
|
-
<Chat messages={messages} user={user} isFlashListEnabled />
|
|
805
|
-
```
|
|
943
|
+
> **Note:** This library no longer depends on `@expo/react-native-action-sheet`. Prefer `messageActions` above. If you specifically want a native action sheet, install it yourself, wrap your tree in `ActionSheetProvider`, and either call `useActionSheet()` in your own `onLongPressMessage` or pass an `actionSheet` prop - the `actionSheet` prop / `context.actionSheet()` escape hatch still works when you provide an implementation. The composer "+" actions use the built-in themed `AttachmentSheet` and need no setup.
|
|
806
944
|
|
|
807
|
-
|
|
945
|
+
### Theming & Dark Mode
|
|
808
946
|
|
|
809
|
-
The chat
|
|
947
|
+
The chat ships with a modern default look and a full token-based theme. Override any subset of tokens via `theme` (light) and `darkTheme` (dark); your overrides are deep-merged over `defaultLightTheme` / `defaultDarkTheme`, and the resolved theme switches at runtime with the color scheme (system, or forced via `colorScheme`). Explicit per-component style props still win over the theme.
|
|
810
948
|
|
|
811
|
-
```
|
|
949
|
+
```tsx
|
|
812
950
|
<Chat
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
autoscrollToBottomThreshold: 0.1,
|
|
817
|
-
animateAutoScrollToBottom: false,
|
|
818
|
-
},
|
|
951
|
+
theme={{
|
|
952
|
+
colors: { accent: '#3390EC', outgoingBubble: '#EFFEDE' },
|
|
953
|
+
radii: { bubble: 18 },
|
|
819
954
|
}}
|
|
955
|
+
darkTheme={{ colors: { background: '#0E1621', incomingBubble: '#182533' } }}
|
|
956
|
+
// colorScheme="dark" // optional: force a scheme instead of following the system
|
|
820
957
|
{...props}
|
|
821
958
|
/>
|
|
822
959
|
```
|
|
823
960
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
- FlashList v2 requires the **New Architecture**. On the old architecture it falls back to a slower JS path.
|
|
827
|
-
- `FlatList`-only knobs (`windowSize`, `maxToRenderPerBatch`, `initialNumToRender`, `updateCellsBatchingPeriod`, `removeClippedSubviews`) are not forwarded to FlashList - it sizes its own render window.
|
|
828
|
-
- If `@shopify/flash-list` is not installed, the prop is ignored, a warning is logged, and `FlatList` is used.
|
|
961
|
+
Token groups: `colors`, `radii`, `spacing`, `typography`, `avatar`, `sendButton`, `composer`, `voice`. Build your own theme-aware components with the exported hooks:
|
|
829
962
|
|
|
830
|
-
|
|
963
|
+
```tsx
|
|
964
|
+
import { useTheme, useThemedStyles } from '@kesha-antonov/react-native-chat'
|
|
965
|
+
import { StyleSheet } from 'react-native'
|
|
966
|
+
|
|
967
|
+
const MyBadge = () => {
|
|
968
|
+
const theme = useTheme()
|
|
969
|
+
const styles = useThemedStyles(t => StyleSheet.create({
|
|
970
|
+
badge: { backgroundColor: t.colors.accent, borderRadius: t.radii.bubble },
|
|
971
|
+
}))
|
|
972
|
+
return <View style={styles.badge} />
|
|
973
|
+
}
|
|
974
|
+
```
|
|
831
975
|
|
|
832
|
-
|
|
976
|
+
Also exported: `defaultLightTheme`, `defaultDarkTheme`, and the `ChatTheme` / `PartialChatTheme` types.
|
|
833
977
|
|
|
834
|
-
|
|
835
|
-
import { useActionSheet } from '@expo/react-native-action-sheet'
|
|
836
|
-
import { setStringAsync } from 'expo-clipboard'
|
|
978
|
+
### Localization (i18n)
|
|
837
979
|
|
|
838
|
-
|
|
980
|
+
All built-in UI strings (composer placeholder, send/cancel, load earlier, today, voice/video/location labels, slide-to-cancel, reply/edit banner, camera-permission text) route through a label table. Built-in translations ship for `en`, `es`, `fr`, `de`, and `ru`, selected by the existing `locale` prop. Override any individual string with `labels`:
|
|
839
981
|
|
|
982
|
+
```tsx
|
|
840
983
|
<Chat
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
buttonIndex => {
|
|
845
|
-
if (buttonIndex === 0)
|
|
846
|
-
setStringAsync(message.text)
|
|
847
|
-
}
|
|
848
|
-
)
|
|
849
|
-
}}
|
|
984
|
+
locale="fr" // pick a built-in translation
|
|
985
|
+
labels={{ placeholder: 'Votre message...' }} // override any string
|
|
986
|
+
{...props}
|
|
850
987
|
/>
|
|
851
988
|
```
|
|
852
989
|
|
|
990
|
+
Exported helpers: `ChatLabels` (type), `defaultLabels`, `translations`, `resolveLabels`, and the `useLabels` hook for reading the resolved labels in custom components.
|
|
991
|
+
|
|
853
992
|
### Message Status
|
|
854
993
|
|
|
855
994
|
Set `sent`, `received`, or `pending` on a message to show its delivery status. By default these render as tick indicators next to the timestamp (`✓` sent, `✓✓` received, `🕓` pending):
|
|
@@ -961,6 +1100,73 @@ module.exports = function override(config, env) {
|
|
|
961
1100
|
|
|
962
1101
|
---
|
|
963
1102
|
|
|
1103
|
+
## ⚡ Performance
|
|
1104
|
+
|
|
1105
|
+
The chat is built for long lists, but a few habits on your side unlock the most:
|
|
1106
|
+
|
|
1107
|
+
**Memoize your render props and config.** Each message row is wrapped in `React.memo` with a comparator that deep-compares the message and reference-compares every other prop. So an unchanged row only skips a re-render when the props you pass it are referentially stable. If you pass inline functions or objects, that row re-renders on every parent render:
|
|
1108
|
+
|
|
1109
|
+
```jsx
|
|
1110
|
+
// ❌ New reference every render - the row can't skip
|
|
1111
|
+
<Chat renderBubble={props => <MyBubble {...props} />} reactions={{ isEnabled: true, onReactionPress }} />
|
|
1112
|
+
|
|
1113
|
+
// ✅ Stable references - unchanged rows skip re-renders
|
|
1114
|
+
const renderBubble = useCallback(props => <MyBubble {...props} />, [])
|
|
1115
|
+
const reactions = useMemo(() => ({ isEnabled: true, onReactionPress }), [onReactionPress])
|
|
1116
|
+
const messageActions = useCallback(message => [{ label: 'Copy', onPress: () => copy(message.text) }], [copy])
|
|
1117
|
+
|
|
1118
|
+
<Chat renderBubble={renderBubble} reactions={reactions} messageActions={messageActions} />
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
This applies to all render props (`renderBubble`, `renderMessageText`, `renderAvatar`, ...), the `reactions` / `audioRecording` / `videoRecording` / `messageActions` config objects, and any style objects.
|
|
1122
|
+
|
|
1123
|
+
**Keep messages immutable.** Update messages by creating new arrays/objects (e.g. `Chat.append(...)`), never by mutating an existing message in place - the row comparator relies on value changes to detect updates.
|
|
1124
|
+
|
|
1125
|
+
**Theme, icons and labels don't need drilling.** `theme` / `darkTheme`, `icons`, and `labels` are read from context (`useTheme`, `useIcons`, `useLabels`), so passing them once on `<Chat>` is enough; they don't cause per-row churn.
|
|
1126
|
+
|
|
1127
|
+
**Tune virtualization if needed.** Sensible `FlatList` defaults ship out of the box (`removeClippedSubviews` on Android, `initialNumToRender`, `maxToRenderPerBatch`, `windowSize`, `updateCellsBatchingPeriod`). `windowSize` is measured in screen-heights (not messages); the default keeps a few screens of content mounted around the viewport. Override any of them via `listProps`:
|
|
1128
|
+
|
|
1129
|
+
```jsx
|
|
1130
|
+
<Chat listProps={{ windowSize: 7, removeClippedSubviews: true }} {...props} />
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
### FlashList (opt-in)
|
|
1134
|
+
|
|
1135
|
+
On long histories `FlatList` can log `VirtualizedList: You have a large list that is slow to update`. [FlashList](https://shopify.github.io/flash-list/) v2 recycles rows instead of keeping them mounted, which removes that class of stall. It is supported as an **optional** dependency - install it yourself and flip one prop:
|
|
1136
|
+
|
|
1137
|
+
```bash
|
|
1138
|
+
yarn add @shopify/flash-list
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
```jsx
|
|
1142
|
+
<Chat messages={messages} user={user} isFlashListEnabled />
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
Everything else keeps working: `isInverted`, the floating day header, `loadEarlierMessagesProps` infinite scroll, the scroll-to-bottom button, and `listProps` (spread last, so it overrides the defaults below).
|
|
1146
|
+
|
|
1147
|
+
The chat sets FlashList's `maintainVisibleContentPosition` for you - `startRenderingFromBottom` when `isInverted={false}`, plus `autoscrollToBottomThreshold: 0.2` so new messages follow the viewport only when you are already at the bottom. Override it through `listProps` if you want different thresholds:
|
|
1148
|
+
|
|
1149
|
+
```jsx
|
|
1150
|
+
<Chat
|
|
1151
|
+
isFlashListEnabled
|
|
1152
|
+
listProps={{
|
|
1153
|
+
maintainVisibleContentPosition: {
|
|
1154
|
+
autoscrollToBottomThreshold: 0.1,
|
|
1155
|
+
animateAutoScrollToBottom: false,
|
|
1156
|
+
},
|
|
1157
|
+
}}
|
|
1158
|
+
{...props}
|
|
1159
|
+
/>
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
Notes:
|
|
1163
|
+
|
|
1164
|
+
- FlashList v2 requires the **New Architecture**. On the old architecture it falls back to a slower JS path.
|
|
1165
|
+
- `FlatList`-only knobs (`windowSize`, `maxToRenderPerBatch`, `initialNumToRender`, `updateCellsBatchingPeriod`, `removeClippedSubviews`) are not forwarded to FlashList - it sizes its own render window.
|
|
1166
|
+
- If `@shopify/flash-list` is not installed, the prop is ignored, a warning is logged, and `FlatList` is used.
|
|
1167
|
+
|
|
1168
|
+
---
|
|
1169
|
+
|
|
964
1170
|
## 🧪 Testing
|
|
965
1171
|
|
|
966
1172
|
<details>
|
package/lib/Actions.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
2
|
import { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
|
+
import { AttachmentAction } from './components/AttachmentSheet';
|
|
3
4
|
export interface ActionsProps {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}>;
|
|
5
|
+
/** Actions shown in the attachment sheet. Add `icon`/`color` for a grid layout. */
|
|
6
|
+
actions?: AttachmentAction[];
|
|
7
|
+
/** Tint color for the action labels in the sheet (defaults to the theme accent). */
|
|
8
8
|
actionSheetOptionTintColor?: string;
|
|
9
9
|
icon?: () => ReactNode;
|
|
10
10
|
wrapperStyle?: StyleProp<ViewStyle>;
|
|
@@ -12,5 +12,5 @@ export interface ActionsProps {
|
|
|
12
12
|
buttonStyle?: StyleProp<ViewStyle>;
|
|
13
13
|
onPressActionButton?(): void;
|
|
14
14
|
}
|
|
15
|
-
export declare function Actions({ actions, actionSheetOptionTintColor, icon, wrapperStyle,
|
|
15
|
+
export declare function Actions({ actions, actionSheetOptionTintColor, icon, wrapperStyle, onPressActionButton, buttonStyle, }: ActionsProps): React.JSX.Element;
|
|
16
16
|
//# sourceMappingURL=Actions.d.ts.map
|
package/lib/Actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Actions.d.ts","sourceRoot":"","sources":["../src/Actions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"Actions.d.ts","sourceRoot":"","sources":["../src/Actions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAyB,MAAM,OAAO,CAAA;AAC/D,OAAO,EAGL,SAAS,EACT,SAAS,EACT,SAAS,EAAE,MAAM,cAAc,CAAA;AACjC,OAAO,EAAE,gBAAgB,EAAmB,MAAM,8BAA8B,CAAA;AAQhF,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,oFAAoF;IACpF,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,IAAI,CAAC,EAAE,MAAM,SAAS,CAAA;IACtB,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IACnC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IACpC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAClC,mBAAmB,CAAC,IAAI,IAAI,CAAA;CAC7B;AAED,wBAAgB,OAAO,CAAE,EACvB,OAAO,EACP,0BAA0B,EAC1B,IAAI,EACJ,YAAY,EACZ,mBAAmB,EACnB,WAAW,GACZ,EAAE,YAAY,qBA2Cd"}
|
package/lib/Actions.js
CHANGED
|
@@ -1,64 +1,53 @@
|
|
|
1
|
-
import React, { useCallback } from 'react';
|
|
2
|
-
import { StyleSheet, View
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { AttachmentSheet } from './components/AttachmentSheet';
|
|
4
|
+
import { Icon } from './components/Icon';
|
|
5
|
+
import { PaperclipIcon } from './components/MediaControls';
|
|
5
6
|
import { TouchableOpacity } from './components/TouchableOpacity';
|
|
7
|
+
import { useThemedStyles } from './hooks/useTheme';
|
|
6
8
|
import stylesCommon from './styles';
|
|
7
|
-
export function Actions({ actions, actionSheetOptionTintColor
|
|
8
|
-
const
|
|
9
|
+
export function Actions({ actions, actionSheetOptionTintColor, icon, wrapperStyle, onPressActionButton, buttonStyle, }) {
|
|
10
|
+
const styles = useThemedStyles(createStyles);
|
|
11
|
+
const [isSheetVisible, setIsSheetVisible] = useState(false);
|
|
9
12
|
const handlePress = useCallback(() => {
|
|
10
13
|
if (onPressActionButton) {
|
|
11
14
|
onPressActionButton();
|
|
12
15
|
return;
|
|
13
16
|
}
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
actionSheet().showActionSheetWithOptions({
|
|
18
|
-
options: titles,
|
|
19
|
-
cancelButtonIndex: titles.length - 1,
|
|
20
|
-
tintColor: actionSheetOptionTintColor,
|
|
21
|
-
}, (buttonIndex) => {
|
|
22
|
-
if (buttonIndex === undefined)
|
|
23
|
-
return;
|
|
24
|
-
const item = actions[buttonIndex];
|
|
25
|
-
item.action?.();
|
|
26
|
-
});
|
|
27
|
-
}, [actionSheet, actions, actionSheetOptionTintColor, onPressActionButton]);
|
|
17
|
+
if (actions?.length)
|
|
18
|
+
setIsSheetVisible(true);
|
|
19
|
+
}, [actions, onPressActionButton]);
|
|
28
20
|
const renderIcon = useCallback(() => {
|
|
29
21
|
if (icon)
|
|
30
22
|
return icon();
|
|
31
|
-
|
|
32
|
-
<
|
|
23
|
+
const size = styles.iconText.fontSize;
|
|
24
|
+
return (<View style={[stylesCommon.centerItems, wrapperStyle]}>
|
|
25
|
+
<Icon name='paperclip' color={styles.iconText.color} size={size} fallback={<PaperclipIcon color={styles.iconText.color} size={size}/>}/>
|
|
33
26
|
</View>);
|
|
34
|
-
}, [icon,
|
|
27
|
+
}, [icon, wrapperStyle, styles]);
|
|
35
28
|
return (<View style={styles.container}>
|
|
36
|
-
<TouchableOpacity onPress={handlePress} style={[styles.button, buttonStyle]}>
|
|
29
|
+
<TouchableOpacity onPress={handlePress} hitSlop={8} style={[styles.button, buttonStyle]}>
|
|
37
30
|
{renderIcon()}
|
|
38
31
|
</TouchableOpacity>
|
|
32
|
+
<AttachmentSheet visible={isSheetVisible} actions={actions ?? []} onClose={() => setIsSheetVisible(false)} tintColor={actionSheetOptionTintColor}/>
|
|
39
33
|
</View>);
|
|
40
34
|
}
|
|
41
|
-
const
|
|
35
|
+
const createStyles = (theme) => StyleSheet.create({
|
|
42
36
|
container: {
|
|
43
|
-
alignItems: '
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
44
39
|
},
|
|
45
40
|
button: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
50
|
-
wrapper: {
|
|
51
|
-
borderColor: Color.defaultColor,
|
|
52
|
-
borderWidth: 2,
|
|
53
|
-
width: 26,
|
|
54
|
-
height: 26,
|
|
55
|
-
borderRadius: 13,
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
paddingBottom: 8,
|
|
56
44
|
},
|
|
45
|
+
// Inset attachment icon: borderless, sized/colored as a field inset control.
|
|
57
46
|
iconText: {
|
|
58
|
-
color:
|
|
47
|
+
color: theme.colors.incomingMeta,
|
|
59
48
|
fontWeight: 'bold',
|
|
60
|
-
fontSize:
|
|
61
|
-
lineHeight:
|
|
49
|
+
fontSize: theme.composer.insetIconSize,
|
|
50
|
+
lineHeight: theme.composer.insetIconSize,
|
|
62
51
|
},
|
|
63
52
|
});
|
|
64
53
|
//# sourceMappingURL=Actions.js.map
|
package/lib/Actions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Actions.js","sourceRoot":"","sources":["../src/Actions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAa,WAAW,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Actions.js","sourceRoot":"","sources":["../src/Actions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAa,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC/D,OAAO,EACL,UAAU,EACV,IAAI,EAGO,MAAM,cAAc,CAAA;AACjC,OAAO,EAAoB,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAChF,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAA;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,YAAY,MAAM,UAAU,CAAA;AAenC,MAAM,UAAU,OAAO,CAAE,EACvB,OAAO,EACP,0BAA0B,EAC1B,IAAI,EACJ,YAAY,EACZ,mBAAmB,EACnB,WAAW,GACE;IACb,MAAM,MAAM,GAAG,eAAe,CAAC,YAAY,CAAC,CAAA;IAC5C,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAE3D,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,IAAI,mBAAmB,EAAE,CAAC;YACxB,mBAAmB,EAAE,CAAA;YACrB,OAAM;QACR,CAAC;QAED,IAAI,OAAO,EAAE,MAAM;YACjB,iBAAiB,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAA;IAElC,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;QAClC,IAAI,IAAI;YACN,OAAO,IAAI,EAAE,CAAA;QAEf,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAA;QACrC,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CACpD;QAAA,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAG,CAAC,EACzI;MAAA,EAAE,IAAI,CAAC,CACR,CAAA;IACH,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;IAEhC,OAAO,CACL,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAC5B;MAAA,CAAC,gBAAgB,CACf,OAAO,CAAC,CAAC,WAAW,CAAC,CACrB,OAAO,CAAC,CAAC,CAAC,CAAC,CACX,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAEpC;QAAA,CAAC,UAAU,EAAE,CACf;MAAA,EAAE,gBAAgB,CAClB;MAAA,CAAC,eAAe,CACd,OAAO,CAAC,CAAC,cAAc,CAAC,CACxB,OAAO,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CACvB,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CACxC,SAAS,CAAC,CAAC,0BAA0B,CAAC,EAE1C;IAAA,EAAE,IAAI,CAAC,CACR,CAAA;AACH,CAAC;AAED,MAAM,YAAY,GAAG,CAAC,KAAgB,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;IAC3D,SAAS,EAAE;QACT,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,QAAQ;KACzB;IACD,MAAM,EAAE;QACN,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,QAAQ;QACxB,aAAa,EAAE,CAAC;KACjB;IACD,6EAA6E;IAC7E,QAAQ,EAAE;QACR,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,YAAY;QAChC,UAAU,EAAE,MAAM;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,aAAa;QACtC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,aAAa;KACzC;CACF,CAAC,CAAA"}
|
package/lib/Avatar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAe,MAAM,OAAO,CAAA;AACrD,OAAO,EACL,UAAU,EAEV,SAAS,EAET,SAAS,EACV,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../src/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAe,MAAM,OAAO,CAAA;AACrD,OAAO,EACL,UAAU,EAEV,SAAS,EAET,SAAS,EACV,MAAM,cAAc,CAAA;AAIrB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAwCzD,MAAM,WAAW,WAAW,CAAC,QAAQ,SAAS,QAAQ;IACpD,cAAc,EAAE,QAAQ,CAAA;IACxB,eAAe,CAAC,EAAE,QAAQ,CAAA;IAC1B,WAAW,CAAC,EAAE,QAAQ,CAAA;IACtB,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,8BAA8B,CAAC,EAAE,OAAO,CAAA;IACxC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;IACvC,cAAc,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAA;IAC1C,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,YAAY,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC,GAAG,SAAS,CAAA;IAC5E,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;IACpC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAA;CACzC;AAED,wBAAgB,MAAM,CAAC,QAAQ,SAAS,QAAQ,GAAG,QAAQ,EACzD,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,4BA0G7B"}
|