@dev-tech/uchat-chat-input
v0.4.6
Published
WhatsApp-style chat input for React Native: emoji & sticker keyboard, attachment menu, photo editor, voice button
Maintainers
Readme
@dev-tech/uchat-chat-input
WhatsApp-style chat input for React Native.
- Emoji keyboard that swaps with the system keyboard at the same height. The input bar stays put while one slides over the other. About 1,640 emoji in Unicode order, with recents, categories, search, skin tones (long press) and whole-emoji backspace (flags, skin tones, joined emoji)
- Stickers: offline emoji packs, or your own image URLs
- Attachment menu: document, camera, gallery, audio
- Photo editor: crop and rotate, draw, text, emoji stickers, per-photo captions, view once
- Send / hold-to-record mic button: slide left to cancel, slide up to lock for hands-free recording; plug in your own recorder
Installation
yarn add @dev-tech/uchat-chat-inputPeer dependencies
Install any your app doesn't already have:
yarn add react-native-svg react-native-safe-area-context \
react-native-keyboard-controller react-native-reanimated \
react-native-view-shot| Package | Used for |
| --- | --- |
| react-native-keyboard-controller (≥ 1.12) | tracking the keyboard frame by frame |
| react-native-reanimated (≥ 3) | animating the space under the input |
| react-native-svg | icons |
| react-native-safe-area-context | bottom inset |
| react-native-view-shot | saving edited photos |
| react-native-image-picker (optional) | defaultPickers: camera and gallery |
| @react-native-documents/picker (optional) | defaultPickers: documents and audio |
The two pickers are only needed if you use defaultPickers. Don't install @react-native-documents/picker alongside the older react-native-document-picker. Both register a native module named RNDocumentPicker. If your app uses the old one, pass your own pickers instead (see below).
Then rebuild the native app: cd ios && pod install, then yarn android / yarn ios.
Setup (once per app)
1. Babel plugin. Reanimated needs its Babel plugin, listed last:
// babel.config.js
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin'], // Reanimated 4
// plugins: ['react-native-reanimated/plugin'], // Reanimated 3
};2. KeyboardProvider at the root:
import { KeyboardProvider } from 'react-native-keyboard-controller';
<SafeAreaProvider>
<KeyboardProvider enabled={false}>
<App />
</KeyboardProvider>
</SafeAreaProvider>;enabled={false} keeps Android's normal adjustResize behaviour on every other screen. ChatInput switches keyboard tracking on while it's mounted and restores the previous setting when it unmounts.
3. Restart Metro with a clean cache after adding the Babel plugin:
yarn start --reset-cacheAndroid: keep android:windowSoftInputMode="adjustResize" on your activity. Don't wrap ChatInput in a KeyboardAvoidingView; it reserves the keyboard's space itself.
Usage
import { ChatInput, Message } from '@dev-tech/uchat-chat-input';
import { defaultPickers } from '@dev-tech/uchat-chat-input/src/pickers';
export function ChatScreen() {
const send = (message: Message) => {
// upload / append to your list
};
return (
<View style={{ flex: 1 }}>
<MessageList style={{ flex: 1 }} />
<ChatInput
recipientName="Stephen"
onSendMessage={send}
pickers={defaultPickers}
/>
</View>
);
}Put ChatInput last in a full-height column. It sizes the space under itself for the keyboard, the emoji panel and the bottom safe-area inset.
What onSendMessage receives
| type | Fields |
| --- | --- |
| text | text |
| attachment | attachment (kind: image / document / audio, uri, name, mimeType, size, width, height), text = caption, viewOnce |
| sticker | sticker (emoji or image) |
| voice | duration (seconds), attachment (kind: 'audio') when a recorder is given |
Picked photos open in the photo editor first. Unedited photos are sent as the original file. Edited photos arrive as a new JPEG.
Props
| Prop | Description |
| --- | --- |
| onSendMessage | Required. Called for every text, file, sticker or voice note |
| recipientName | Required. Shown on the photo editor's send row |
| value, onChangeText | Control the text yourself (needed for @mentions). Omit both and the input keeps its own text |
| inputRef | Your own TextInput ref |
| onSelectionChange, onFocus | Forwarded to the TextInput |
| header | Rendered inside the input above the text, e.g. a reply preview |
| pickers | { openCamera, openGallery, pickDocument, pickAudio }, each returning Promise<Attachment[]> ([] when cancelled). Without it the clip and camera buttons are hidden |
| recorder | Your audio recorder, { start, stop, cancel } (see below). The input keeps its own recording UI (timer, waveform, slide to cancel) and calls these. Without it, recording is only simulated |
| voiceButton | Replaces the whole mic button and its recording UI while the input is empty |
| stickers | Show the sticker tab (default true) |
| emojis | Show the emoji tab (default true). With only one of emojis / stickers on, the panel shows just that one without tabs; with both off the emoji button is hidden |
| stickerSize | Sticker width and height in dp (default 64, never larger than its cell) |
| stickerColumns | Stickers per row (default 4) |
| viewOnce | Show the view-once toggle in the photo editor (default true) |
| barStyle | Style merged over the input bar's default (12 sides, 6 top, 0 bottom — the bottom is reserved separately), e.g. { paddingHorizontal: 6 } |
| minBottomInset | Least space under the bar when the keyboard and panels are closed; the larger of this and the safe-area inset is used (default 6) |
| keyboardGap | Space kept between the bar and the system keyboard's top edge, like WhatsApp's thin gap (default 8) |
| emojiStorage | { getItem, setItem } (sync or async, e.g. AsyncStorage or MMKV). Remembers recent emoji and chosen skin tones across app restarts; in memory otherwise |
| placeholder | Default "Message" |
Using your own pickers
const pickers: AttachmentPickers = {
openCamera: async () => toAttachments(await myCamera()),
openGallery: async () => toAttachments(await myGallery()),
pickDocument: async () => toAttachments(await myFilePicker()),
pickAudio: async () => toAttachments(await myFilePicker({ audio: true })),
};Using your own recorder
const recorder: VoiceRecorderAdapter = {
start: () => myRecorder.start(),
// Resolve to null if nothing usable was recorded
stop: async () => {
const file = await myRecorder.stop();
return file && { uri: file.uri, duration: file.seconds, mimeType: 'audio/m4a' };
},
cancel: () => myRecorder.cancel(),
};
<ChatInput recorder={recorder} onSendMessage={send} recipientName="Stephen" />;Hold the mic to record, release to send, slide left to cancel. Slide up to the lock to keep recording without holding; the bar then shows a delete button and the mic turns into Send. A tap shorter than a second is cancelled and shows "Hold to record, release to send". The recording arrives as a voice message with duration and an audio attachment.
Emoji search, skin tones and recents
- Search: the 🔍 button on the emoji panel opens a search field with a row of results above the keyboard. Results match names and keywords and insert into the message; closing the keyboard returns to the emoji panel.
- Skin tones: emoji with a small corner mark take a skin tone. Long press to pick one; the grid then shows that emoji in your tone.
- Recents and tones survive restarts when you pass
emojiStorage:
// uchat: MMKV. Create it once (outside the component)
import {globalStorage} from '@dev-tech/uchat-shared-lib/src/hooks/common/useStorage';
const emojiStorage = {
getItem: (key: string) => globalStorage.getString(key),
setItem: (key: string, value: string) => globalStorage.set(key, value),
};
<ChatInput emojiStorage={emojiStorage} ... />;Other exports
MessageBubble, StickerView, MediaEditor, MediaViewer, EmojiKeyboard, EmojiPicker, EmojiSearch, StickerPicker, AttachmentMenu, useEmojiKeyboard, insertAtSelection, deleteBackward, searchEmojis, withSkinTone, setEmojiStorage, STICKER_PACKS, and the types Message, Attachment, AttachmentPickers, EmojiStorage, Sticker, ChatInputProps.
Emoji names and keywords are adapted from the rn-emoji-keyboard dataset (MIT, © The Widlarz Group); the package doesn't depend on it.
Using it in uchat
integration/uchat/MessagesScreen.tsx is uchat's MessagesScreen with its input replaced by ChatInput. It keeps the existing handleSendMessage, @mentions, ReplyBubble and VoiceRecorder. Before shipping it, check toUchatAttachments against what the old DocumentPicker sent.
Troubleshooting
Cannot read property 'KeyboardProvider' of undefined or Failed to create a worklet. Metro is serving code built without the Babel plugin, or the app binary predates the native packages. Stop Metro, then run yarn start --reset-cache. If it persists, rebuild the app.
... doesn't seem to be linked. The native app wasn't rebuilt after installing the peer dependencies.
Development
This repo is also a demo app (App.tsx → src/screens/ChatScreen.tsx).
yarn install
yarn start --reset-cache
yarn android # or: cd ios && pod install && yarn ios
yarn testPublishing
npm login # as a member of the dev-tech org
npm version patch # or minor / major
npm publish # builds lib/ via prepack