Repository: FaridSafi/react-native-gifted-chat Branch: master Commit: e63ff1bf4a4e Files: 193 Total size: 452.1 KB Directory structure: gitextract_6i4y2rb6/ ├── .expo-shared/ │ └── assets.json ├── .github/ │ ├── FUNDING.yml │ ├── copilot-instructions.md │ ├── stale.yml │ └── workflows/ │ ├── main.yml │ └── stale.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .npmignore ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── babel.config.cjs ├── codecov.yml ├── eslint.config.js ├── example/ │ ├── .gitignore │ ├── README.md │ ├── app/ │ │ ├── (tabs)/ │ │ │ ├── _layout.tsx │ │ │ ├── explore.tsx │ │ │ └── index.tsx │ │ ├── _layout.tsx │ │ ├── chat/ │ │ │ ├── _layout.tsx │ │ │ ├── basic.tsx │ │ │ ├── customized-rendering.tsx │ │ │ ├── links.tsx │ │ │ ├── reply.tsx │ │ │ └── slack.tsx │ │ └── modal.tsx │ ├── app.json │ ├── babel.config.js │ ├── components/ │ │ ├── chat-examples/ │ │ │ ├── BasicExample.tsx │ │ │ ├── CustomizedRenderingExample.tsx │ │ │ ├── LinksExample.tsx │ │ │ ├── ReplyExample.tsx │ │ │ └── SlackExample.tsx │ │ ├── external-link.tsx │ │ ├── haptic-tab.tsx │ │ ├── hello-wave.tsx │ │ ├── parallax-scroll-view.tsx │ │ ├── themed-text.tsx │ │ ├── themed-view.tsx │ │ └── ui/ │ │ ├── collapsible.tsx │ │ ├── icon-symbol.ios.tsx │ │ └── icon-symbol.tsx │ ├── constants/ │ │ └── theme.ts │ ├── eslint.config.js │ ├── example-expo/ │ │ ├── AccessoryBar.tsx │ │ ├── CustomActions.tsx │ │ ├── CustomView/ │ │ │ ├── index.tsx │ │ │ ├── index.web.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ ├── data/ │ │ │ ├── earlierMessages.ts │ │ │ └── messages.ts │ │ └── mediaUtils.ts │ ├── example-gifted-chat/ │ │ ├── README.md │ │ └── src/ │ │ ├── Chats.tsx │ │ ├── InputToolbar.tsx │ │ ├── customComponents.tsx │ │ └── messages.ts │ ├── example-slack-message/ │ │ ├── README.md │ │ └── src/ │ │ ├── SlackBubble.tsx │ │ └── SlackMessage.tsx │ ├── hooks/ │ │ ├── use-color-scheme.ts │ │ ├── use-color-scheme.web.ts │ │ ├── use-theme-color.ts │ │ └── useKeyboardVerticalOffset.ts │ ├── ios/ │ │ ├── .gitignore │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── Podfile.properties.json │ │ ├── example/ │ │ │ ├── Images.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ ├── SplashScreen.storyboard │ │ │ └── Supporting/ │ │ │ └── Expo.plist │ │ ├── example.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── example.xcscheme │ │ └── example.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── metro.config.js │ ├── package.json │ ├── scripts/ │ │ └── reset-project.js │ ├── styles/ │ │ └── index.ts │ ├── tsconfig.json │ └── utils/ │ └── styleUtils.ts ├── expoSnack/ │ ├── ExpoSnack.tsx │ ├── README.md │ └── package.json ├── jest.config.cjs ├── package.json ├── src/ │ ├── Actions.tsx │ ├── Avatar.tsx │ ├── Bubble/ │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Color.ts │ ├── Composer.tsx │ ├── Constant.ts │ ├── Day/ │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── GiftedAvatar.tsx │ ├── GiftedChat/ │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── GiftedChatContext.ts │ ├── InputToolbar.tsx │ ├── LoadEarlierMessages.tsx │ ├── Message/ │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── MessageAudio.tsx │ ├── MessageImage.tsx │ ├── MessageReply.tsx │ ├── MessageText.tsx │ ├── MessageVideo.tsx │ ├── MessagesContainer/ │ │ ├── components/ │ │ │ ├── DayAnimated/ │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.ts │ │ │ │ └── types.ts │ │ │ └── Item/ │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── Models.ts │ ├── QuickReplies.tsx │ ├── Reply/ │ │ ├── index.ts │ │ └── types.ts │ ├── ReplyPreview.tsx │ ├── Send.tsx │ ├── SystemMessage.tsx │ ├── Time.tsx │ ├── TypingIndicator/ │ │ ├── index.tsx │ │ ├── styles.ts │ │ └── types.ts │ ├── __tests__/ │ │ ├── Actions.test.tsx │ │ ├── Avatar.test.tsx │ │ ├── Bubble.test.tsx │ │ ├── Color.test.tsx │ │ ├── Composer.test.tsx │ │ ├── Constant.test.tsx │ │ ├── Day.test.tsx │ │ ├── DayAnimated.test.tsx │ │ ├── GiftedAvatar.test.tsx │ │ ├── GiftedChat.test.tsx │ │ ├── InputToolbar.test.tsx │ │ ├── LoadEarlier.test.tsx │ │ ├── Message.test.tsx │ │ ├── MessageImage.test.tsx │ │ ├── MessageReply.test.tsx │ │ ├── MessageText.test.tsx │ │ ├── MessagesContainer.test.tsx │ │ ├── ReplyPreview.test.tsx │ │ ├── Send.test.tsx │ │ ├── SystemMessage.test.tsx │ │ ├── Time.test.tsx │ │ ├── __snapshots__/ │ │ │ ├── Actions.test.tsx.snap │ │ │ ├── Avatar.test.tsx.snap │ │ │ ├── Bubble.test.tsx.snap │ │ │ ├── Color.test.tsx.snap │ │ │ ├── Composer.test.tsx.snap │ │ │ ├── Constant.test.tsx.snap │ │ │ ├── Day.test.tsx.snap │ │ │ ├── DayAnimated.test.tsx.snap │ │ │ ├── GiftedAvatar.test.tsx.snap │ │ │ ├── GiftedChat.test.tsx.snap │ │ │ ├── InputToolbar.test.tsx.snap │ │ │ ├── LoadEarlier.test.tsx.snap │ │ │ ├── Message.test.tsx.snap │ │ │ ├── MessageImage.test.tsx.snap │ │ │ ├── MessageReply.test.tsx.snap │ │ │ ├── MessageText.test.tsx.snap │ │ │ ├── ReplyPreview.test.tsx.snap │ │ │ ├── Send.test.tsx.snap │ │ │ ├── SystemMessage.test.tsx.snap │ │ │ └── Time.test.tsx.snap │ │ ├── data.ts │ │ └── utils.test.ts │ ├── components/ │ │ ├── MessageReply.tsx │ │ ├── ReplyPreview.tsx │ │ └── TouchableOpacity.tsx │ ├── hooks/ │ │ ├── useColorScheme.ts │ │ └── useUpdateLayoutEffect.ts │ ├── index.ts │ ├── linkParser.tsx │ ├── logging.ts │ ├── styles.ts │ ├── types.ts │ └── utils.ts ├── tests/ │ └── setup.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .expo-shared/assets.json ================================================ { "5c6d215cbde93d15ae63d2ea43dfe8bf8a79a53146382cf7f3f0089bec2fc5d6": true, "d0e86e9f72936ac85597d9cd6415cf22a3c208505d5586ac04de09d4dd305707": true, "b884dbf3daca9d0a4de2f6552aa4dbdda9cbff26e2677bd76a514d71af2e7e2c": true, "30bf2d1edfc90d2841794660cf30a94bb134b89d4808bd4b05cac2304cf6fad7": true, "01d8b00b4e3d1dfab70e1ef3354b373f13bdcc3ad29122b3afacf34afd043960": true, "36cb6cfb9a281169f9ba1eb7c345fba1d56a0c7115fbf8c4b3753427aad8edaa": true, "3232d6cbd4824ece99982787e431ff1425df1d22288961602506b046c50bc516": true, "5c8d230c038116f9327c1a38157e7b5d25e1d6bbfbb0ba4e86310f097c3d0f9f": true, "250d0d32ab3051aee4b8f9d26a3299e6b3d8e6ee137dffe8a7e183e5478a2040": true } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [faridsafi, kesha-antonov, xcarpentier, johan-dutoit] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a custom sponsorship URL ================================================ FILE: .github/copilot-instructions.md ================================================ # React Native Gifted Chat The most complete chat UI for React Native & Web. This is a TypeScript React Native component library with example applications demonstrating usage across React Native, React Native Web, and Expo platforms. Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. ## Working Effectively ### Bootstrap and build the repository: - `yarn install` -- NEVER CANCEL: takes 58 seconds. Set timeout to 120+ seconds. - `yarn build` -- builds TypeScript library, takes 3 seconds. Set timeout to 30+ seconds. - `yarn lint` -- lints source code, takes 3 seconds. Currently has warnings but no errors. Set timeout to 30+ seconds. - `yarn test` -- NEVER CANCEL: runs Jest test suite, takes 9 seconds. Set timeout to 60+ seconds. ### Full validation before publishing: - `yarn prepublishOnly` -- NEVER CANCEL: runs lint + build + test, takes 11 seconds total. Set timeout to 60+ seconds. ### Known Issues: - If tests fail due to snapshot mismatches after fresh dependency install, run `yarn test -u` to update snapshots - Snapshot tests may need updates when React Native or dependency versions change ### Example app development: - `cd example && yarn install` -- NEVER CANCEL: takes 38 seconds. Set timeout to 90+ seconds. - Install Expo CLI globally: `npm install -g @expo/cli` or use `npx expo` commands - Native development: `cd example && npx expo start` - Web development: `cd example && npx expo start --web` (requires additional dependencies) - The example app starts Metro bundler on http://localhost:8081 - Expect dependency version warnings in offline/CI mode - these are normal ### Type checking and development: - `yarn tsc:watch` -- runs TypeScript compiler in watch mode for development - `yarn tsc:write` -- compiles TypeScript and writes output to /lib directory ## Requirements and Setup ### System Requirements: - Node.js >= 18 (tested with 20.19.4) - Yarn package manager 1.22.22+ (do NOT use npm for this project) - TypeScript compiler (included in devDependencies) ### Dependencies are already installed if yarn.lock exists The repository includes both package-lock.json and yarn.lock but ALWAYS use yarn commands, never npm commands. ## Validation ### Always run full validation before completing changes: 1. `yarn lint` -- check for code style issues (warnings are acceptable, errors are not) 2. `yarn build` -- verify TypeScript compilation succeeds 3. `yarn test` -- NEVER CANCEL: ensure all 19 test suites and 29 tests pass. Takes 31 seconds. ### Manual validation scenarios: After making code changes, you should test basic functionality by: 1. Building the library: `yarn build` 2. Running the test suite: `yarn test` 3. For UI changes: Start the example app with `cd example && npx expo start --web` (if web dependencies are installed) or `npx expo start` for native development 4. ALWAYS test that the TypeScript declarations in /lib are correctly generated ### Testing approach: - All tests are located in `src/__tests__/` directory - Tests use Jest with React Test Renderer - Test coverage can be viewed with `yarn test:coverage` - Snapshot tests are used extensively (27 snapshots) ## Project Structure ### Key directories: - `/src` -- main library source code (TypeScript) - `/lib` -- compiled JavaScript output (generated by `yarn build`, do not edit manually) - `/example` -- example React Native app demonstrating library usage - `/src/__tests__` -- Jest test files - `/.github/workflows/main.yml` -- CI/CD pipeline (tests Node 18 and 20) ### Important files: - `package.json` -- main project configuration and scripts - `tsconfig.json` -- TypeScript compiler configuration - `jest.config.cjs` -- Jest test configuration - `.eslintrc.cjs` -- ESLint linting rules - `babel.config.cjs` -- Babel transformation configuration - `example/package.json` -- example app dependencies ### Build output: The `yarn build` command generates JavaScript files and TypeScript declaration files in the `/lib` directory. This directory should not be edited manually and is ignored by git but included in npm package. ## Common Tasks ### Making code changes: 1. Edit source files in `/src` directory 2. Run `yarn lint` to check style 3. Run `yarn build` to compile TypeScript 4. Run `yarn test` to verify tests pass 5. Test manually with example app if UI changes ### Adding or modifying tests: - Tests are in `src/__tests__/` directory - Follow existing test patterns using React Test Renderer - Update snapshots if needed with `yarn test -u` - Ensure all tests pass with `yarn test` ### Working with the example app: - Example app uses Expo and demonstrates library functionality - Install dependencies: `cd example && yarn install` - Start development server: `npx expo start` - For web: `npx expo start --web` (requires react-native-web and @expo/metro-runtime) ## CI/CD Integration The repository uses GitHub Actions (`.github/workflows/main.yml`) that: - Tests on Node.js versions 18 and 20 - Runs `yarn install` and `yarn build` - Build typically takes 1-2 minutes on CI Always ensure your changes pass the full validation pipeline locally before pushing. ## Common Command Reference ### Root directory (library development): ```bash yarn install # Install dependencies (58 seconds) yarn build # Build TypeScript library (3 seconds) yarn lint # Lint source code (3 seconds) yarn test # Run test suite (9 seconds) yarn prepublishOnly # Full validation pipeline (11 seconds) yarn tsc:watch # TypeScript watch mode ``` ### Example directory (app development): ```bash cd example yarn install # Install example dependencies (38 seconds) npx expo start # Start native development server npx expo start --web # Start web development server ``` ### Time expectations (NEVER CANCEL these operations): - yarn install: 58 seconds (root), 41 seconds (example) - yarn build: 3 seconds - yarn lint: 3 seconds - yarn test: 9 seconds (after fresh install and snapshot updates) - yarn prepublishOnly: 11 seconds total Always set timeouts to at least double these times to account for system variations. ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 15 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > Sorry, but this issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. BTW Thank you for your contributions 😀 !!! # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/main.yml ================================================ name: Main CI on: pull_request: branches: - master jobs: checks: runs-on: ubuntu-latest strategy: matrix: node-version: [20, 22] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Node modules run: | yarn install - name: Lint run: | yarn lint - name: Type Check run: | yarn tsc --noEmit - name: Build run: | yarn build - name: Test run: | yarn test ================================================ FILE: .github/workflows/stale.yml ================================================ name: Close stale issues on: schedule: - cron: '0 0 * * *' # runs daily jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-issue-stale: 365 days-before-issue-close: 0 stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed now.' close-issue-message: 'Closing due to inactivity for over a year.' only-issue-labels: '' ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/ .expo/ npm-debug.log lib/ TODO.md .idea .vscode Exponent-*.app *.log coverage/ web-build/ .eslintcache # Yarn .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions yarn-error.log example_bare/vendor example_bare/**/build example_bare/ios/Pods example_bare/android/.gradle ================================================ FILE: .husky/pre-commit ================================================ yarn lint-staged ================================================ FILE: .npmignore ================================================ .expo/ .expo-shared/ .circleci/ .github/ .vscode/ example/ example-expo/ example-slack-message/ example-gifted-chat/ screenshots/ babel.config.js tests/ README.md ISSUE_TEMPLATE.md codecov.yml media/ App.tsx app.json metro.config.js src/ tsconfig.json tslint.json yarn.lock flow-typedefs/ .flowconfig yarn-error.log web-build/ types.d.ts ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [3.3.2] - 2026-01-22 ### 🐛 Bug Fixes - Fixed `React.memo` and `React.forwardRef` components not rendering correctly when passed as render props - `renderComponentOrElement` now properly handles components with `$$typeof` property - Fixed layout jump on initial render - content now renders with `opacity: 0` until initialized - Fixed keyboard vertical offset documentation and examples ### 🔧 Improvements - Updated `keyboardVerticalOffset` documentation in README with clearer explanation - Added `hidden` style for smoother initial render transitions ### 📝 Documentation - Improved `keyboardVerticalOffset` section explaining that it equals distance from screen top to container top - Added recommendation to use `useHeaderHeight()` from `@react-navigation/elements` ## [3.3.0] - 2026-01-21 ### ✨ Features - **Swipe to Reply**: New swipe-to-reply functionality using `ReanimatedSwipeable` (based on #2692) - Replaced deprecated `Swipeable` with `ReanimatedSwipeable` from react-native-gesture-handler - Added `reply` prop to `GiftedChat` with grouped configuration options - Swipe direction support: `'left'` (swipe left, icon on right) or `'right'` (swipe right, icon on left) - Custom swipe action rendering via `renderAction` - Built-in animated `ReplyIcon` component - `ReplyPreview` component with smooth enter/exit animations - Reply message display in `Bubble` component via `messageReply` prop - **New Props**: - `scrollToBottomContentStyle` - style for scroll to bottom button content ### 🐛 Bug Fixes - Fixed #2702 - typing issues - Fixed #2708 - component issues - Fixed #2607 - edge case handling - Fixed #2701 - rendering issues - Fixed #2691 - prop handling - Fixed #2688 - style issues - Fixed #2687 - component behavior - Fixed #2618 - scroll issues - Fixed #2677, #2682, #2602 - multiple fixes - Fixed #2684, #2686 - component issues - Fixed `onScroll` type definition - Fixed messages padding - Fixed SystemMessage styles - Added missing worklets for animations - Removed `ts-expect-error` for `requestAnimationFrame` (now properly typed for React Native) - Fixed two typing issues (#2698) ### 🔧 Improvements - Grouped reply-related props into `ReplyProps` interface for cleaner API - Added `SwipeToReplyProps` for Message-level swipe configuration - Added `BubbleReplyProps` for Bubble-level reply message styling - Added example app to lint command with proper path alias support - Improved reply animations (enter/exit transitions) - Changes from #2705 ### 📝 Documentation - Updated README with swipe-to-reply feature documentation and examples - Updated license link - Added reply message implementation example (#2690) ### 🧪 Testing - Updated test snapshots - Added tests for `MessageReply` component - Added tests for `ReplyPreview` component ## [3.2.3] - 2025-12-XX ### 🐛 Bug Fixes - Fixed `onScroll` type definition ## [3.2.0] - 2025-11-25 ### ✨ Features - **Custom Link Parser**: Replaced `react-native-autolink` dependency with custom link parser implementation for better control and performance - Removed external dependency on `react-native-autolink` - Improved link parsing with custom implementation in `linkParser.tsx` - Updated `MessageText` component to use new parser - Enhanced links example in example app ### 🐛 Bug Fixes - Adjusted message bubble styles for better rendering - Updated test snapshots to reflect parser changes ## [3.1.5] - 2025-11-25 ### ✨ Features - **Color Scheme Support**: Added `colorScheme` prop to `GiftedChat` component - New `useColorScheme` hook for consistent color scheme handling - Automatically adapts UI elements (Composer, InputToolbar, Send) based on color scheme - Added comprehensive tests for color scheme functionality ### 📝 Documentation - Updated README with `colorScheme` prop documentation ## [3.1.4] - 2025-11-25 ### 🐛 Bug Fixes - Added left padding to `TextInput` when no accessory is present for better visual alignment - Adjusted input toolbar styles for improved layout ## [3.1.3] - 2025-11-25 ### 🔧 Improvements - Removed unused imports for cleaner codebase ## [3.1.2] - 2025-11-24 ### 🐛 Bug Fixes - Fixed message bubble styles for small messages - Improved rendering of compact message content ### 🧪 Testing - Updated test snapshots ## [3.1.1] - 2025-11-24 ### 🐛 Bug Fixes - Fixed Bubble component styles for better message rendering - Corrected style inconsistencies in message bubbles ### 🧪 Testing - Updated test snapshots to reflect style fixes ## [3.1.0] - 2025-11-24 ### 🔧 Improvements - Refactored component styles for better maintainability - Updated Expo Snack example with latest changes ### 🧪 Testing - Updated test snapshots ## [3.0.1] - 2025-11-24 ### 🐛 Bug Fixes - Fixed Composer auto-resize height behavior on web platform ### 🧪 Testing - Updated test snapshots ## [3.0.0] - 2025-11-23 This is a major release with significant breaking changes, new features, and improvements. The library has been completely rewritten in TypeScript with improved type safety, better keyboard handling, and enhanced customization options. ### 🚨 Breaking Changes #### Renamed Props (GiftedChat) - `onInputTextChanged` → moved to `textInputProps.onChangeText` (follows React Native naming pattern) - `alwaysShowSend` → `isSendButtonAlwaysVisible` (consistent boolean naming convention) - `onPress` → `onPressMessage` (more specific naming) - `onLongPress` → `onLongPressMessage` (more specific naming) - `options` → `actions` (better semantic naming, different type signature) - `optionTintColor` → `actionSheetOptionTintColor` (clearer naming) - `renderUsernameOnMessage` → `isUsernameVisible` (consistent boolean naming) - `showUserAvatar` → `isUserAvatarVisible` (consistent boolean naming) - `showAvatarForEveryMessage` → `isAvatarVisibleForEveryMessage` (consistent boolean naming) - `renderAvatarOnTop` → `isAvatarOnTop` (consistent boolean naming) - `focusOnInputWhenOpeningKeyboard` → `shouldFocusInputOnKeyboardOpen` (consistent boolean naming) - `messageContainerRef` → `messagesContainerRef` (typo fix) - `alignTop` → `isAlignedTop` (consistent boolean naming) - `inverted` → `isInverted` (consistent boolean naming) #### Removed Props (GiftedChat) - `bottomOffset` - use `keyboardAvoidingViewProps.keyboardVerticalOffset` instead - `disableKeyboardController` - removed keyboard controller configuration - `isKeyboardInternallyHandled` - keyboard handling now always uses react-native-keyboard-controller - `lightboxProps` - custom Modal implementation replaced react-native-lightbox-v2 - `placeholder` - moved to `textInputProps.placeholder` - `disableComposer` - moved to `textInputProps.editable={false}` - `keyboardShouldPersistTaps` - moved to `listProps.keyboardShouldPersistTaps` - `maxInputLength` - moved to `textInputProps.maxLength` - `extraData` - moved to `listProps.extraData` - `infiniteScroll` - use `loadEarlierMessagesProps.isInfiniteScrollEnabled` instead - `parsePatterns` - removed, automatic link parsing improved #### Props Moved to MessagesContainer (via spreading) These props moved from `GiftedChatProps` to `MessagesContainerProps` but are still accessible on `GiftedChat` via prop spreading: - `messages` - now in MessagesContainerProps - `isTyping` - now in MessagesContainerProps (via TypingIndicatorProps) - `loadEarlier` → `loadEarlierMessagesProps.isAvailable` - `isLoadingEarlier` → `loadEarlierMessagesProps.isLoading` - `onLoadEarlier` → `loadEarlierMessagesProps.onPress` - `renderLoadEarlier` - now in MessagesContainerProps - `renderDay` - now in MessagesContainerProps - `renderMessage` - now in MessagesContainerProps - `renderFooter` - now in MessagesContainerProps - `renderChatEmpty` - now in MessagesContainerProps - `scrollToBottomStyle` - now in MessagesContainerProps - `isScrollToBottomEnabled` - now in MessagesContainerProps - `scrollToBottomComponent` - now in MessagesContainerProps - `onQuickReply` - now in MessagesContainerProps - `listViewProps` → `listProps` (renamed in MessagesContainerProps) #### Type Signature Changes - `options`: changed from `{ [key: string]: () => void }` to `Array<{ title: string, action: () => void }>` - `textInputProps`: changed from `object` to `Partial>` - `renderInputToolbar`: now accepts `React.ComponentType | React.ReactElement | function | null` (can be component, element, function, or null) - All callback props now use arrow function syntax instead of function syntax for better type inference #### Dependency Changes - Removed `react-native-lightbox-v2` (replaced with custom Modal implementation) - Removed `react-native-iphone-x-helper` (deprecated) - Removed `react-native-keyboard-controller` as direct dependency - Added `react-native-keyboard-controller` as peer dependency (>=1.0.0) - Added `react-native-gesture-handler` as peer dependency (>=2.0.0) - Added `react-native-reanimated` support for v3 & v4 - Added `react-native-safe-area-context` as peer dependency (>=5.0.0) ### ✨ New Features #### TypeScript Migration - Complete conversion from JavaScript to TypeScript/TSX - Improved type safety and IntelliSense support - Better type definitions for all components and props - Refactored types to arrow functions for better readability #### Keyboard Handling - New `keyboardTopToolbarHeight` prop for better keyboard customization - New `keyboardAvoidingViewProps` to pass props to KeyboardAvoidingView from react-native-keyboard-controller - Improved keyboard behavior and offset handling - Consolidated keyboard configuration (removed individual keyboard props in favor of `keyboardAvoidingViewProps`) - Fixed auto-grow text input behavior - Better keyboard open/close transitions - New `OverKeyboardView` component for MessageImage to keep keyboard open #### Message Rendering - `isDayAnimationEnabled` prop to control day separator animations - Support for passing custom components in render functions - Improved message parsing with better link detection - Parse links in system messages (fixes #2105) - Better phone number parsing with custom matchers support - Improved URL parsing (email, phone, URL detection) #### UI & Styling - Dark theme support in example app - Safe area provider included in library - Improved LoadEarlier messages logic - Better themed styles implementation - Fixed press animation for TouchableOpacity - Replaced deprecated `TouchableWithoutFeedback` with `Pressable` - Better scroll to bottom button behavior on Android #### Image Viewing - Custom Modal implementation replacing react-native-lightbox-v2 - Better image viewing experience with proper insets handling - Improved MessageImage component #### Accessibility & UX - `renderTicks` prop for message status indicators - Better scroll to bottom wrapper visibility handling - `useCallbackThrottled` for improved scroll performance - Allow passing children to SystemMessage - Improved load earlier messages functionality ### 🐛 Bug Fixes - Fixed duplicate paragraph tags in README - Fixed scroll to bottom when `isScrollToBottomEnabled=false` (#2652) - Fixed TypeScript type inconsistencies and ESLint errors (#2653) - Fixed automatic scroll to bottom issues (#2630, #2621, #2644) - Fixed DayAnimated test import and added proper test coverage for renderDay prop - Fixed not passed `isDayAnimationEnabled` prop - Fixed MessageContainer scroll to bottom press on Android - Fixed safer change ScrollToBottomWrapper visibility - Fixed dependency cycles in imports - Fixed MessageText container style - Fixed reanimated issue in MessageContainer - Fixed construct messages on send in example - Fixed web support in example - Fixed #2659 (memoization issues) - Fixed #2640 (various bug fixes) - Fixed show location in example - Fixed errors in keyboard handling - Fixed load earlier messages functionality - Fixed Bubble type parameter to re-enable generics on message prop (#2639) - Fixed listViewProps typing with Partial (#2628) - Fixed MessageContainer to add renderDay prop and insert DayAnimated Component (#2632) - Fixed dateFormatCalendar default value in README ### 🔧 Improvements #### Performance - Memoized values & functions for better performance - Better scroll performance with throttled callbacks - Optimized re-renders #### Code Quality - Added ESLint with import sorting - Fixed all examples with ESLint - Improved code structure and organization - Better error handling - Cleaner prop passing and component structure #### Testing - All tests converted to TypeScript - Updated snapshots for new components - Run tests in correct timezone (Europe/Paris) - Improved test coverage - Added comprehensive copilot instructions with validated commands #### Documentation - Improved README structure and formatting - Better prop documentation and grouping - Added matchers example - Added working Expo Snack link - Better feature documentation - Added maintainer section - Improved previews and images - Added export documentation - Fixed formatting issues and typos - Better keyboard props documentation #### Example App - Updated to latest React Native and Expo - Added tabs with different chat examples - Added working link to Expo Snack - Better example organization - Added dark theme support - Removed padding from bottom of toolbar - Added custom phone matcher example - Switch to dev build in README - Android: transparent navigation & status bars by default - Better project structure with multiple example types #### Build & Development - Better dependency management - Updated to Node.js >= 20 - Yarn 1.22.22+ as package manager - Added stale workflow for issue management - Script to rebuild native dependencies - Improved local development setup ### 📦 Dependencies #### Added - `@expo/react-native-action-sheet`: ^4.1.1 - `@types/lodash.isequal`: ^4.5.8 - `dayjs`: ^1.11.19 - `lodash.isequal`: ^4.5.0 - `react-native-zoom-reanimated`: ^1.4.10 #### Peer Dependencies (now required) - `react`: >=18.0.0 - `react-native`: * - `react-native-gesture-handler`: >=2.0.0 - `react-native-keyboard-controller`: >=1.0.0 - `react-native-reanimated`: >=3.0.0 || ^4.0.0 - `react-native-safe-area-context`: >=5.0.0 ### 🔄 Migration Guide #### Update Prop Names ```javascript // Before (v2.8.1) // After (v3.0.0) ``` #### Install Peer Dependencies ```bash npm install react-native-gesture-handler react-native-keyboard-controller react-native-reanimated react-native-safe-area-context # or yarn add react-native-gesture-handler react-native-keyboard-controller react-native-reanimated react-native-safe-area-context ``` #### Update Image Lightbox The library now uses a custom Modal implementation instead of react-native-lightbox-v2. If you were customizing the lightbox, you'll need to update your customization approach. ### 📝 Notes - This version includes 170+ commits since v2.8.1 - Full TypeScript support with improved type definitions - Better React Native compatibility (tested with RN 0.81.5) - Improved React 19 support - Better Expo integration ### 👥 Contributors Special thanks to all contributors who made this release possible, including fixes and improvements from the community. --- For detailed commit history, see: https://github.com/FaridSafi/react-native-gifted-chat/compare/2.8.1...3.0.0 ================================================ FILE: ISSUE_TEMPLATE.md ================================================ #### Issue Description [FILL THIS OUT] #### Steps to Reproduce / Code Snippets [FILL THIS OUT] #### Expected Results [FILL THIS OUT] #### Additional Information * Nodejs version: [FILL THIS OUT] * React version: [FILL THIS OUT] * React Native version: [FILL THIS OUT] * react-native-gifted-chat version: [FILL THIS OUT] * Platform(s) (iOS, Android, or both?): [FILL THIS OUT] * TypeScript version: [FILL THIS OUT] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 Farid from Safi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

npm version npm downloads build platforms TypeScript Expo compatible

React Native Gifted Chat

The most complete chat UI for React Native & Web

Try GiftedChat on Expo Snack

--- ## ✨ Features - 🎨 **Fully Customizable** - Override any component with your own implementation - 📎 **Composer Actions** - Attach photos, files, or trigger custom actions - ↩️ **Reply to Messages** - Swipe-to-reply with reply preview and message threading - ⏮️ **Load Earlier Messages** - Infinite scroll with pagination support - 📋 **Copy to Clipboard** - Long-press messages to copy text - 🔗 **Smart Link Parsing** - Auto-detect URLs, emails, phone numbers, hashtags, mentions - 👤 **Avatars** - User initials or custom avatar images - 🌍 **Localized Dates** - Full i18n support via Day.js - ⌨️ **Keyboard Handling** - Smart keyboard avoidance for all platforms - 💬 **System Messages** - Display system notifications in chat - ⚡ **Quick Replies** - Bot-style quick reply buttons - ✍️ **Typing Indicator** - Show when users are typing - ✅ **Message Status** - Tick indicators for sent/delivered/read states - ⬇️ **Scroll to Bottom** - Quick navigation button - 🌐 **Web Support** - Works with react-native-web - 📱 **Expo Support** - Easy integration with Expo projects - 📝 **TypeScript** - Complete TypeScript definitions included

         

---

Sponsors

Le Reacteur - Coding Bootcamp in Paris co-founded by Farid Safi
Stream - Scalable chat API/Server written in Go (API Tour | Tutorial)
Ethora - A complete app engine featuring GiftedChat (GitHub)

📚 React Key Concepts (2nd ed.)

--- ## 📖 Table of Contents - [Features](#-features) - [Requirements](#-requirements) - [Installation](#-installation) - [Usage](#-usage) - [Props Reference](#-props-reference) - [Data Structure](#-data-structure) - [Platform Notes](#-platform-notes) - [Example App](#-example-app) - [Troubleshooting](#-troubleshooting) - [Contributing](#-contributing) - [Authors](#-authors) - [License](#-license) --- ## 📋 Requirements | Requirement | Version | |-------------|---------| | React Native | >= 0.70.0 | | iOS | >= 13.4 | | Android | API 21+ (Android 5.0) | | Expo | SDK 50+ | | TypeScript | >= 5.0 (optional) | --- ## 📦 Installation ### Expo Projects ```bash npx expo install react-native-gifted-chat react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controller ``` ### Bare React Native Projects **Step 1:** Install the packages Using yarn: ```bash yarn add react-native-gifted-chat react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controller ``` Using npm: ```bash npm install --save react-native-gifted-chat react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controller ``` **Step 2:** Install iOS pods ```bash npx pod-install ``` **Step 3:** Configure react-native-reanimated Follow the [react-native-reanimated installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/#step-2-add-reanimateds-babel-plugin) to add the Babel plugin. --- ## 🚀 Usage ### Basic Example ```jsx import React, { useState, useCallback, useEffect } from 'react' import { GiftedChat } from 'react-native-gifted-chat' import { useHeaderHeight } from '@react-navigation/elements' export function Example() { const [messages, setMessages] = useState([]) // keyboardVerticalOffset = distance from screen top to GiftedChat container // useHeaderHeight() returns status bar + navigation header height const headerHeight = useHeaderHeight() useEffect(() => { setMessages([ { _id: 1, text: 'Hello developer', createdAt: new Date(), user: { _id: 2, name: 'John Doe', avatar: 'https://placeimg.com/140/140/any', }, }, ]) }, []) const onSend = useCallback((messages = []) => { setMessages(previousMessages => GiftedChat.append(previousMessages, messages), ) }, []) return ( onSend(messages)} user={{ _id: 1, }} keyboardAvoidingViewProps={{ keyboardVerticalOffset: headerHeight }} /> ) } ``` > **💡 Tip:** Check out more examples in the [`example`](example) directory including Slack-style messages, quick replies, and custom components. --- ## 📊 Data Structure Messages, system messages, and quick replies follow the structure defined in [Models.ts](src/Models.ts).
Message Object Structure ```typescript interface IMessage { _id: string | number text: string createdAt: Date | number user: User image?: string video?: string audio?: string system?: boolean sent?: boolean received?: boolean pending?: boolean quickReplies?: QuickReplies } interface User { _id: string | number name?: string avatar?: string | number | (() => React.ReactNode) } ```
--- ## 📖 Props Reference ### Core Configuration - **`messages`** _(Array)_ - Messages to display - **`user`** _(Object)_ - User sending the messages: `{ _id, name, avatar }` - **`onSend`** _(Function)_ - Callback when sending a message - **`messageIdGenerator`** _(Function)_ - Generate an id for new messages. Defaults to a simple random string generator. - **`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'`) - **`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`. ### Refs - **`messagesContainerRef`** _(FlatList ref)_ - Ref to the flatlist - **`textInputRef`** _(TextInput ref)_ - Ref to the text input ### Keyboard & Layout - **`keyboardProviderProps`** _(Object)_ - Props to be passed to the [`KeyboardProvider`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-provider) for keyboard handling. Default values: - `statusBarTranslucent: true` - Required on Android for correct keyboard height calculation when status bar is translucent (edge-to-edge mode) - `navigationBarTranslucent: true` - Required on Android for correct keyboard height calculation when navigation bar is translucent (edge-to-edge mode) - **`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. - **`isAlignedTop`** _(Boolean)_ Controls whether or not the message bubbles appear at the top of the chat (Default is false - bubbles align to bottom) - **`isInverted`** _(Bool)_ - Reverses display order of `messages`; default is `true` #### Understanding `keyboardVerticalOffset` The [`keyboardVerticalOffset`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view#keyboardverticaloffset) tells the KeyboardAvoidingView where its container starts relative to the top of the screen. This is essential when GiftedChat is not positioned at the very top of the screen (e.g., when you have a navigation header). **Default value:** `insets.top` (status bar height from `useSafeAreaInsets()`). This works correctly only when GiftedChat fills the entire screen without a navigation header. If you have a navigation header, you need to pass the correct offset via `keyboardAvoidingViewProps`. **What the value means:** The offset equals the distance (in points) from the top of the screen to the top of your GiftedChat container. This typically includes: - Status bar height - Navigation header height (on iOS, `useHeaderHeight()` already includes status bar) **How to use:** ```jsx import { useHeaderHeight } from '@react-navigation/elements' function ChatScreen() { // useHeaderHeight() returns status bar + navigation header height on iOS const headerHeight = useHeaderHeight() return ( ) } ``` > **Note:** `useHeaderHeight()` requires your chat component to be rendered inside a proper navigation screen (not conditional rendering). If it returns `0`, ensure your chat screen is a real navigation screen with a visible header. **Why this matters:** Without the correct offset, the keyboard may overlap the input field or leave extra space. The KeyboardAvoidingView uses this value to calculate how much to shift the content when the keyboard appears. ### Text Input & Composer - **`text`** _(String)_ - Input text; default is `undefined`, but if specified, it will override GiftedChat's internal state. Useful for managing text state outside of GiftedChat (e.g. with Redux). Don't forget to implement `textInputProps.onChangeText` to update the text state. - **`initialText`** _(String)_ - Initial text to display in the input field - **`isSendButtonAlwaysVisible`** _(Bool)_ - Always show send button in input text composer; default `false`, show only when text input is not empty - **`isTextOptional`** _(Bool)_ - Allow sending messages without text (useful for media-only messages); default `false`. Use with `isSendButtonAlwaysVisible` for media attachments. - **`minComposerHeight`** _(Object)_ - Custom min-height of the composer. - **`maxComposerHeight`** _(Object)_ - Custom max height of the composer. - **`minInputToolbarHeight`** _(Integer)_ - Minimum height of the input toolbar; default is `44` - **`renderInputToolbar`** _(Component | Function)_ - Custom message composer container - **`renderComposer`** _(Component | Function)_ - Custom text input message composer - **`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/FaridSafi/react-native-gifted-chat/pull/487)) - **`renderActions`** _(Component | Function)_ - Custom action button on the left of the message composer - **`renderAccessory`** _(Component | Function)_ - Custom second line of actions below the message composer - **`textInputProps`** _(Object)_ - props to be passed to the [``](https://reactnative.dev/docs/textinput). ### Actions & Action Sheet - **`onPressActionButton`** _(Function)_ - Callback when the Action button is pressed (if set, the default `actionSheet` will not be used) - **`actionSheet`** _(Function)_ - Custom action sheet interface for showing action options - **`actions`** _(Array)_ - Custom action options for the input toolbar action button; array of objects with `title` (string) and `action` (function) properties - **`actionSheetOptionTintColor`** _(String)_ - Tint color for action sheet options ### Messages & Message Container - **`messagesContainerStyle`** _(Object)_ - Custom style for the messages container - **`renderMessage`** _(Component | Function)_ - Custom message container - **`renderLoading`** _(Component | Function)_ - Render a loading view when initializing - **`renderChatEmpty`** _(Component | Function)_ - Custom component to render in the ListView when messages are empty - **`renderChatFooter`** _(Component | Function)_ - Custom component to render below the MessagesContainer (separate from the ListView) - **`listProps`** _(Object)_ - Extra props to be passed to the messages [``](https://reactnative.dev/docs/flatlist). Supports all FlatList props including `maintainVisibleContentPosition` for keeping scroll position when new messages arrive (useful for AI chatbots). ### Message Bubbles & Content - **`renderBubble`** _(Component | Function(`props: BubbleProps`))_ - Custom message bubble. Receives [BubbleProps](src/Bubble/types.ts) as parameter. - **`renderMessageText`** _(Component | Function)_ - Custom message text - **`renderMessageImage`** _(Component | Function)_ - Custom message image - **`renderMessageVideo`** _(Component | Function)_ - Custom message video - **`renderMessageAudio`** _(Component | Function)_ - Custom message audio - **`renderCustomView`** _(Component | Function)_ - Custom view inside the bubble - **`isCustomViewBottom`** _(Bool)_ - Determine whether renderCustomView is displayed before or after the text, image and video views; default is `false` - **`onPressMessage`** _(Function(`context`, `message`))_ - Callback when a message bubble is pressed - **`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) - **`imageProps`** _(Object)_ - Extra props to be passed to the [``](https://reactnative.dev/docs/image) component created by the default `renderMessageImage` - **`imageStyle`** _(Object)_ - Custom style for message images - **`videoProps`** _(Object)_ - Extra props to be passed to the video component created by the required `renderMessageVideo` - **`messageTextProps`** _(Object)_ - Extra props to be passed to the MessageText component. Useful for customizing link parsing behavior, text styles, and matchers. Supports the following props: - `matchers` - Custom matchers for linking message content (like URLs, phone numbers, hashtags, mentions) - `linkStyle` - Custom style for links - `email` - Enable/disable email parsing (default: true) - `phone` - Enable/disable phone number parsing (default: true) - `url` - Enable/disable URL parsing (default: true) - `hashtag` - Enable/disable hashtag parsing (default: false) - `mention` - Enable/disable mention parsing (default: false) - `hashtagUrl` - Base URL for hashtags (e.g., 'https://x.com/hashtag') - `mentionUrl` - Base URL for mentions (e.g., 'https://x.com') - `stripPrefix` - Strip 'http://' or 'https://' from URL display (default: false) - `TextComponent` - Custom Text component to use (e.g., from react-native-gesture-handler) Example: ```tsx { return replacerArgs[0].replace(/[\-\(\) ]/g, '') }, getLinkText: (replacerArgs: ReplacerArgs): string => { return replacerArgs[0] }, style: styles.linkStyle, onPress: (match: CustomMatch) => { const url = match.getAnchorHref() const options: { title: string action?: () => void }[] = [ { title: 'Copy', action: () => setStringAsync(url) }, { title: 'Call', action: () => Linking.openURL(`tel:${url}`) }, { title: 'Send SMS', action: () => Linking.openURL(`sms:${url}`) }, { title: 'Cancel' }, ] showActionSheetWithOptions({ options: options.map(o => o.title), cancelButtonIndex: options.length - 1, }, (buttonIndex?: number) => { if (buttonIndex === undefined) return const option = options[buttonIndex] option.action?.() }) }, }, ], linkStyle: { left: { color: 'blue' }, right: { color: 'lightblue' } }, }} /> ``` See full example in [LinksExample](example/components/chat-examples/LinksExample.tsx) ### Avatars - **`renderAvatar`** _(Component | Function)_ - Custom message avatar; set to `null` to not render any avatar for the message - **`isUserAvatarVisible`** _(Bool)_ - Whether to render an avatar for the current user; default is `false`, only show avatars for other users - **`isAvatarVisibleForEveryMessage`** _(Bool)_ - When false, avatars will only be displayed when a consecutive message is from the same user on the same day; default is `false` - **`onPressAvatar`** _(Function(`user`))_ - Callback when a message avatar is tapped - **`onLongPressAvatar`** _(Function(`user`))_ - Callback when a message avatar is long-pressed - **`isAvatarOnTop`** _(Bool)_ - Render the message avatar at the top of consecutive messages, rather than the bottom; default is `false` ### Username - **`isUsernameVisible`** _(Bool)_ - Indicate whether to show the user's username inside the message bubble; default is `false` - **`renderUsername`** _(Component | Function)_ - Custom Username container ### Date & Time - **`timeFormat`** _(String)_ - Format to use for rendering times; default is `'LT'` (see [Day.js Format](https://day.js.org/docs/en/display/format)) - **`dateFormat`** _(String)_ - Format to use for rendering dates; default is `'D MMMM'` (see [Day.js Format](https://day.js.org/docs/en/display/format)) - **`dateFormatCalendar`** _(Object)_ - Format to use for rendering relative times; default is `{ sameDay: '[Today]' }` (see [Day.js Calendar](https://day.js.org/docs/en/plugin/calendar)) - **`renderDay`** _(Component | Function)_ - Custom day above a message - **`dayProps`** _(Object)_ - Props to pass to the Day component: - `containerStyle` - Custom style for the day container - `wrapperStyle` - Custom style for the day wrapper - `textProps` - Props to pass to the Text component (e.g., `style`, `allowFontScaling`, `numberOfLines`) - **`renderTime`** _(Component | Function)_ - Custom time inside a message - **`timeTextStyle`** _(Object)_ - Custom text style for time inside messages (supports left/right styles) - **`isDayAnimationEnabled`** _(Bool)_ - Enable animated day label that appears on scroll; default is `true` ### System Messages - **`renderSystemMessage`** _(Component | Function)_ - Custom system message ### Load Earlier Messages - **`loadEarlierMessagesProps`** _(Object)_ - Props to pass to the LoadEarlierMessages component. The button is only visible when `isAvailable` is `true`. Supports the following props: - `isAvailable` - Controls button visibility (default: false) - `onPress` - Callback when button is pressed - `isLoading` - Display loading indicator (default: false) - `isInfiniteScrollEnabled` - Enable infinite scroll up when reaching the top of messages container, automatically calls `onPress` (not yet supported for web) - `label` - Override the default "Load earlier messages" text - `containerStyle` - Custom style for the button container - `wrapperStyle` - Custom style for the button wrapper - `textStyle` - Custom style for the button text - `activityIndicatorStyle` - Custom style for the loading indicator - `activityIndicatorColor` - Color of the loading indicator (default: 'white') - `activityIndicatorSize` - Size of the loading indicator (default: 'small') - **`renderLoadEarlier`** _(Component | Function)_ - Custom "Load earlier messages" button ### Typing Indicator - **`isTyping`** _(Bool)_ - Typing Indicator state; default `false`. If you use`renderFooter` it will override this. - **`renderTypingIndicator`** _(Component | Function)_ - Custom typing indicator component - **`typingIndicatorStyle`** _(StyleProp)_ - Custom style for the TypingIndicator component. - **`renderFooter`** _(Component | Function)_ - Custom footer component on the ListView, e.g. `'User is typing...'`; see [CustomizedFeaturesExample.tsx](example/components/chat-examples/CustomizedFeaturesExample.tsx) for an example. Overrides default typing indicator that triggers when `isTyping` is true. ### Quick Replies See [Quick Replies example in messages.ts](example/example-expo/data/messages.ts) - **`onQuickReply`** _(Function)_ - Callback when sending a quick reply (to backend server) - **`renderQuickReplies`** _(Function)_ - Custom all quick reply view - **`quickReplyStyle`** _(StyleProp)_ - Custom quick reply view style - **`quickReplyTextStyle`** _(StyleProp)_ - Custom text style for quick reply buttons - **`quickReplyContainerStyle`** _(StyleProp)_ - Custom container style for quick replies - **`renderQuickReplySend`** _(Function)_ - Custom quick reply **send** view ### Reply to Messages Gifted Chat supports swipe-to-reply functionality out of the box. When enabled, users can swipe on a message to reply to it, displaying a reply preview in the input toolbar and the replied message above the new message bubble. > **Note:** This feature uses `ReanimatedSwipeable` from `react-native-gesture-handler` and `react-native-reanimated` for smooth, performant animations. #### Basic Usage ```tsx ``` #### Reply Props (Grouped) The `reply` prop accepts an object with the following structure: ```typescript interface ReplyProps { // Swipe gesture configuration swipe?: { isEnabled?: boolean // Enable swipe-to-reply; default false direction?: 'left' | 'right' // Swipe direction; default 'left' onSwipe?: (message: TMessage) => void // Callback when swiped renderAction?: ( // Custom swipe action component progress: SharedValue, translation: SharedValue, position: 'left' | 'right' ) => React.ReactNode actionContainerStyle?: StyleProp } // Reply preview styling (above input toolbar) previewStyle?: { containerStyle?: StyleProp textStyle?: StyleProp imageStyle?: StyleProp } // In-bubble reply styling messageStyle?: { containerStyle?: StyleProp containerStyleLeft?: StyleProp containerStyleRight?: StyleProp textStyle?: StyleProp textStyleLeft?: StyleProp textStyleRight?: StyleProp imageStyle?: StyleProp } // Callbacks and state message?: ReplyMessage // Controlled reply state onClear?: () => void // Called when reply cleared onPress?: (message: TMessage) => void // Called when reply preview tapped // Custom renderers renderPreview?: (props: ReplyPreviewProps) => React.ReactNode renderMessageReply?: (props: MessageReplyProps) => React.ReactNode } ``` #### ReplyMessage Structure When a message has a reply, it includes a `replyMessage` property: ```typescript interface ReplyMessage { _id: string | number text: string user: User image?: string audio?: string } ``` #### Advanced Example with External State ```tsx const [replyMessage, setReplyMessage] = useState(null) { const newMessages = messages.map(msg => ({ ...msg, replyMessage: replyMessage || undefined, })) setMessages(prev => GiftedChat.append(prev, newMessages)) setReplyMessage(null) }} user={{ _id: 1 }} reply={{ swipe: { isEnabled: true, direction: 'right', onSwipe: setReplyMessage, }, message: replyMessage, onClear: () => setReplyMessage(null), onPress: (msg) => scrollToMessage(msg._id), }} /> ``` #### Smooth Animations The reply preview automatically animates when: - **Appearing**: Smoothly expands from zero height with fade-in effect - **Disappearing**: Smoothly collapses with fade-out effect - **Content changes**: Smoothly transitions when replying to a different message These animations use `react-native-reanimated` for 60fps performance. ### Scroll to Bottom - **`isScrollToBottomEnabled`** _(Bool)_ - Enables the scroll to bottom Component (Default is false) - **`scrollToBottomComponent`** _(Function)_ - Custom Scroll To Bottom Component container - **`scrollToBottomOffset`** _(Integer)_ - Custom Height Offset upon which to begin showing Scroll To Bottom Component (Default is 200) - **`scrollToBottomStyle`** _(Object)_ - Custom style for Scroll To Bottom wrapper (position, bottom, right, etc.) - **`scrollToBottomContentStyle`** _(Object)_ - Custom style for Scroll To Bottom content (size, background, shadow, etc.) ### Maintaining Scroll Position (AI Chatbots) For AI chat interfaces where long responses arrive and you don't want to disrupt the user's reading position, use [`maintainVisibleContentPosition`](https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition) via `listProps`: ```tsx // Basic usage - always maintain scroll position // With auto-scroll threshold - auto-scroll if within 10 pixels of newest content // Conditionally enable based on scroll state (recommended for chatbots) const [isScrolledUp, setIsScrolledUp] = useState(false) { setIsScrolledUp(event.contentOffset.y > 50) }, maintainVisibleContentPosition: isScrolledUp ? { minIndexForVisible: 0, autoscrollToTopThreshold: 10 } : undefined, }} /> ``` --- ## 📱 Platform Notes ### Android
Keyboard configuration If you are using Create React Native App / Expo, no Android specific installation steps are required. Otherwise, we recommend modifying your project configuration: Make sure you have `android:windowSoftInputMode="adjustResize"` in your `AndroidManifest.xml`: ```xml ``` For **Expo**, you can append `KeyboardAvoidingView` after GiftedChat (Android only): ```jsx {Platform.OS === 'android' && } ```
### Web (react-native-web)
With create-react-app 1. Install react-app-rewired: `yarn add -D react-app-rewired` 2. Create `config-overrides.js`: ```js module.exports = function override(config, env) { config.module.rules.push({ test: /\.js$/, exclude: /node_modules[/\\](?!react-native-gifted-chat)/, use: { loader: 'babel-loader', options: { babelrc: false, configFile: false, presets: [ ['@babel/preset-env', { useBuiltIns: 'usage' }], '@babel/preset-react', ], plugins: ['@babel/plugin-proposal-class-properties'], }, }, }) return config } ``` > **Examples:** > - [xcarpentier/gifted-chat-web-demo](https://github.com/xcarpentier/gifted-chat-web-demo) > - [Gatsby example](https://github.com/xcarpentier/clean-archi-boilerplate/tree/develop/apps/web)
--- ## 🧪 Testing
Triggering layout events in tests `TEST_ID` is exported as constants that can be used in your testing library of choice. Gifted Chat uses `onLayout` to determine the height of the chat container. To trigger `onLayout` during your tests: ```typescript const WIDTH = 200 const HEIGHT = 2000 const loadingWrapper = getByTestId(TEST_ID.LOADING_WRAPPER) fireEvent(loadingWrapper, 'layout', { nativeEvent: { layout: { width: WIDTH, height: HEIGHT, }, }, }) ```
--- ## 📦 Example App The repository includes a comprehensive example app demonstrating all features: ```bash # Clone and install git clone https://github.com/FaridSafi/react-native-gifted-chat.git cd react-native-gifted-chat/example yarn install # Run on iOS npx expo run:ios # Run on Android npx expo run:android # Run on Web npx expo start --web ``` The example app showcases: - 💬 Basic chat functionality - 🎨 Custom message bubbles and avatars - ↩️ Reply to messages with swipe gesture - ⚡ Quick replies (bot-style) - ✍️ Typing indicators - 📎 Attachment actions - 🔗 Link parsing and custom matchers - 🌐 Web compatibility --- ## ❓ Troubleshooting
TextInput is hidden on Android Make sure you have `android:windowSoftInputMode="adjustResize"` in your `AndroidManifest.xml`. See [Android configuration](#android) above.
How to set Bubble color for each user? See [this issue](https://github.com/FaridSafi/react-native-gifted-chat/issues/672) for examples.
How to customize InputToolbar styles? See [this issue](https://github.com/FaridSafi/react-native-gifted-chat/issues/662) for examples.
How to manually dismiss the keyboard? See [this issue](https://github.com/FaridSafi/react-native-gifted-chat/issues/647) for examples.
How to use renderLoading? See [this issue](https://github.com/FaridSafi/react-native-gifted-chat/issues/298) for examples.
--- ## 🤔 Have a Question? 1. Check this README first 2. Search [existing issues](https://github.com/FaridSafi/react-native-gifted-chat/issues) 3. Ask on [StackOverflow](https://stackoverflow.com/questions/tagged/react-native-gifted-chat) 4. Open a new issue if needed --- ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Install dependencies (`yarn install`) 4. Make your changes 5. Run tests (`yarn test`) 6. Run linting (`yarn lint`) 7. Build the library (`yarn build`) 8. Commit your changes (`git commit -m 'Add amazing feature'`) 9. Push to the branch (`git push origin feature/amazing-feature`) 10. Open a Pull Request ### Development Setup ```bash # Install dependencies yarn install # Build the library yarn build # Run tests yarn test # Run linting yarn lint # Full validation yarn prepublishOnly ``` --- ## 👥 Authors **Original Author:** [Farid Safi](https://www.x.com/FaridSafi) **Co-author:** [Xavier Carpentier](https://www.x.com/xcapetir) - [Hire Xavier](https://xaviercarpentier.com) **Maintainer:** [Kesha Antonov](https://github.com/kesha-antonov) > I've been maintaining this project for 2 years, completely in my free time and without any compensation. If you find it helpful, please consider [becoming a sponsor](https://github.com/sponsors/kesha-antonov) to support continued development. 💖 --- ## 📄 License [MIT](LICENSE) ---

Built with ❤️ by the React Native community

================================================ FILE: babel.config.cjs ================================================ module.exports = function (api) { api.cache(true) return { presets: [ '@babel/preset-env', 'module:@react-native/babel-preset', '@babel/preset-typescript', ], plugins: [ '@babel/plugin-transform-unicode-property-regex', '@babel/plugin-transform-react-jsx', 'react-native-reanimated/plugin', ], } } ================================================ FILE: codecov.yml ================================================ coverage: status: patch: default: off ================================================ FILE: eslint.config.js ================================================ import stylistic from '@stylistic/eslint-plugin' import typescriptEslint from '@typescript-eslint/eslint-plugin' import typescriptParser from '@typescript-eslint/parser' import importPlugin from 'eslint-plugin-import' import jestPlugin from 'eslint-plugin-jest' import perfectionistPlugin from 'eslint-plugin-perfectionist' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' export default [ { ignores: [ '**/node_modules/**', '**/lib/**', '**/build/**', '**/.expo/**', '**/android/**', '**/ios/**', // Config files 'example/*.js', 'example/*.config.js', 'example/scripts/**', ], }, { files: ['src/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parser: typescriptParser, parserOptions: { ecmaFeatures: { jsx: true, }, }, globals: { fetch: 'readonly', navigator: 'readonly', __DEV__: 'readonly', XMLHttpRequest: 'readonly', FormData: 'readonly', React$Element: 'readonly', requestAnimationFrame: 'readonly', // Node.js globals for build scripts and configuration files require: 'readonly', module: 'readonly', process: 'readonly', global: 'readonly', console: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', // Jest globals describe: 'readonly', test: 'readonly', it: 'readonly', jest: 'readonly', expect: 'readonly', beforeAll: 'readonly', beforeEach: 'readonly', afterAll: 'readonly', afterEach: 'readonly', }, }, plugins: { '@stylistic': stylistic, '@typescript-eslint': typescriptEslint, 'import': importPlugin, 'perfectionist': perfectionistPlugin, 'react': react, 'react-hooks': reactHooks, }, settings: { react: { version: 'detect', }, 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'], }, 'import/resolver': { typescript: { alwaysTryTypes: true, project: './tsconfig.json', }, node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }, 'import/core-modules': ['react', 'react-native'], }, rules: { // Import rules 'import/no-unresolved': 'error', 'import/named': 'error', 'import/default': 'error', 'import/namespace': 'error', 'import/export': 'error', 'import/no-absolute-path': 'error', 'import/no-self-import': 'error', 'import/no-cycle': 'warn', 'import/no-useless-path-segments': 'error', 'import/no-duplicates': 'error', 'import/first': 'error', 'import/newline-after-import': 'warn', 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], // React rules 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': 'off', 'react/display-name': 'off', 'react/prop-types': 'off', // React Hooks 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: '(useAnimatedStyle|useSharedValue|useAnimatedGestureHandler|useAnimatedScrollHandler|useAnimatedProps|useDerivedValue|useAnimatedRef|useAnimatedReact|useAnimatedReaction|useCallbackDebounced|useCallbackThrottled)', }, ], // TypeScript rules '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['error'], // Stylistic rules '@stylistic/semi': ['error', 'never'], '@stylistic/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none', requireLast: true, }, singleline: { delimiter: 'comma', requireLast: false, }, }, ], '@stylistic/indent': [ 'error', 2, { SwitchCase: 1, VariableDeclarator: 'first', ignoredNodes: ['TemplateLiteral'], }, ], '@stylistic/quotes': ['error', 'single'], '@stylistic/jsx-quotes': ['error', 'prefer-single'], '@stylistic/comma-dangle': [ 'error', { arrays: 'always-multiline', objects: 'always-multiline', imports: 'always-multiline', exports: 'never', functions: 'never', }, ], '@stylistic/arrow-parens': ['error', 'as-needed'], '@stylistic/template-curly-spacing': 'off', '@stylistic/linebreak-style': ['off', 'unix'], '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], '@stylistic/jsx-closing-bracket-location': ['error', 'line-aligned'], // General rules 'no-func-assign': 'off', 'no-class-assign': 'off', 'no-useless-escape': 'off', 'no-unused-vars': 'off', // Use @typescript-eslint/no-unused-vars instead 'no-unreachable': 'error', 'curly': [2, 'multi', 'consistent'], 'nonblock-statement-body-position': ['error', 'below'], // Perfectionist rules 'perfectionist/sort-imports': [ 'error', { groups: [ 'react', 'external', 'internal', ['parent', 'sibling'], 'index', ], customGroups: { value: { react: ['^react$', '^react-native$'], }, }, newlinesBetween: 'ignore', }, ], 'perfectionist/sort-interfaces': 'off', }, }, // Example app configuration with path aliases { files: ['example/**/*.{js,jsx,ts,tsx}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parser: typescriptParser, parserOptions: { ecmaFeatures: { jsx: true, }, project: './example/tsconfig.json', }, globals: { fetch: 'readonly', navigator: 'readonly', __DEV__: 'readonly', XMLHttpRequest: 'readonly', FormData: 'readonly', React$Element: 'readonly', requestAnimationFrame: 'readonly', require: 'readonly', module: 'readonly', process: 'readonly', global: 'readonly', console: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', }, }, plugins: { '@stylistic': stylistic, '@typescript-eslint': typescriptEslint, 'import': importPlugin, 'perfectionist': perfectionistPlugin, 'react': react, 'react-hooks': reactHooks, }, settings: { react: { version: 'detect', }, 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'], }, 'import/resolver': { typescript: { alwaysTryTypes: true, project: './example/tsconfig.json', }, node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }, 'import/core-modules': ['react', 'react-native', 'expo-router', 'expo-blur', 'expo-haptics', 'expo-symbols', 'expo-system-ui', 'expo-web-browser', 'expo-font', 'expo-splash-screen', 'expo-status-bar', 'react-native-gifted-chat'], }, rules: { // Import rules 'import/no-unresolved': 'error', 'import/named': 'error', 'import/default': 'error', 'import/namespace': 'error', 'import/export': 'error', 'import/no-absolute-path': 'error', 'import/no-self-import': 'error', 'import/no-cycle': 'warn', 'import/no-useless-path-segments': 'error', 'import/no-duplicates': 'error', 'import/first': 'error', 'import/newline-after-import': 'warn', 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], // React rules 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': 'off', 'react/display-name': 'off', 'react/prop-types': 'off', // React Hooks 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: '(useAnimatedStyle|useSharedValue|useAnimatedGestureHandler|useAnimatedScrollHandler|useAnimatedProps|useDerivedValue|useAnimatedRef|useAnimatedReact|useAnimatedReaction|useCallbackDebounced|useCallbackThrottled)', }, ], // TypeScript rules '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['error'], // Stylistic rules '@stylistic/semi': ['error', 'never'], '@stylistic/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none', requireLast: true, }, singleline: { delimiter: 'comma', requireLast: false, }, }, ], '@stylistic/indent': [ 'error', 2, { SwitchCase: 1, VariableDeclarator: 'first', ignoredNodes: ['TemplateLiteral'], }, ], '@stylistic/quotes': ['error', 'single'], '@stylistic/jsx-quotes': ['error', 'prefer-single'], '@stylistic/comma-dangle': [ 'error', { arrays: 'always-multiline', objects: 'always-multiline', imports: 'always-multiline', exports: 'never', functions: 'never', }, ], '@stylistic/arrow-parens': ['error', 'as-needed'], '@stylistic/template-curly-spacing': 'off', '@stylistic/linebreak-style': ['off', 'unix'], '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], '@stylistic/jsx-closing-bracket-location': ['error', 'line-aligned'], // General rules 'no-func-assign': 'off', 'no-class-assign': 'off', 'no-useless-escape': 'off', 'no-unused-vars': 'off', 'no-unreachable': 'error', 'curly': [2, 'multi', 'consistent'], 'nonblock-statement-body-position': ['error', 'below'], // Perfectionist rules 'perfectionist/sort-imports': [ 'error', { groups: [ 'react', 'external', 'internal', ['parent', 'sibling'], 'index', ], customGroups: { value: { react: ['^react$', '^react-native$'], }, }, newlinesBetween: 'ignore', }, ], 'perfectionist/sort-interfaces': 'off', }, }, { files: ['tests/**/*', 'src/__tests__/**/*'], plugins: { jest: jestPlugin, }, rules: { ...jestPlugin.configs.recommended.rules, }, }, ] ================================================ FILE: example/.gitignore ================================================ # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files # dependencies node_modules/ # Expo .expo/ dist/ web-build/ expo-env.d.ts # Native .kotlin/ *.orig.* *.jks *.p8 *.p12 *.key *.mobileprovision # Metro .metro-health-check* # debug npm-debug.* yarn-debug.* yarn-error.* # macOS .DS_Store *.pem # local env files .env*.local # typescript *.tsbuildinfo app-example # generated native folders /ios /android ================================================ FILE: example/README.md ================================================ # How to use 1. Install dependencies: ```bash yarn install cd example yarn install ``` 2. Install iOS dev build (only needed once or after rebuilding native dependencies): ```bash yarn installDevBuild:ios ``` or ```bash yarn installDevBuild:android ``` 3. Start the example app on iOS simulator: ```bash yarn start:ios ``` # Dark Theme Support To switch theme press `Cmd + Shift + A` in iOS simulator. ================================================ FILE: example/app/(tabs)/_layout.tsx ================================================ import React from 'react' import { Tabs } from 'expo-router' import { HapticTab } from '@/components/haptic-tab' import { IconSymbol } from '@/components/ui/icon-symbol' import { Colors } from '@/constants/theme' import { useColorScheme } from '@/hooks/use-color-scheme' export default function TabLayout () { const colorScheme = useColorScheme() return ( , }} /> , }} /> ) } ================================================ FILE: example/app/(tabs)/explore.tsx ================================================ import React from 'react' import { ScrollView, StyleSheet, View } from 'react-native' import { useRouter } from 'expo-router' import { RectButton } from 'react-native-gesture-handler' import { SafeAreaView } from 'react-native-safe-area-context' 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' const examples: Array<{ id: ChatExample, title: string, description: string }> = [ { id: 'basic', title: 'Basic Example', description: 'Basic chat with keyboard logging for testing' }, { id: 'links', title: 'Links & Patterns', description: 'Phone numbers, emails, URLs, hashtags, and mentions' }, { id: 'customized-rendering', title: 'Customized Rendering', description: 'Customized chat with all rendering options' }, { id: 'slack', title: 'Slack Style', description: 'Slack-like message styling' }, { id: 'reply', title: 'Reply Example', description: 'Example demonstrating reply functionality' }, ] export default function ExploreScreen () { const router = useRouter() const backgroundColor = useThemeColor({}, 'background') const borderColor = useThemeColor({ light: '#e0e0e0', dark: '#444' }, 'icon') return ( Explore Chat Examples Choose from different chat implementations to see various features and styling options. {examples.map(example => ( router.push(`/chat/${example.id}`)} > {example.title} {example.description} Try it → ))} ) } const styles = StyleSheet.create({ fill: { flex: 1, }, titleContainer: { padding: 20, paddingBottom: 10, }, description: { paddingHorizontal: 20, paddingBottom: 20, }, examplesContainer: { padding: 20, gap: 15, }, exampleCard: { padding: 20, borderRadius: 12, borderWidth: 1, gap: 8, }, exampleTitle: { marginBottom: 4, }, exampleDescription: { opacity: 0.7, marginBottom: 8, }, tryButton: { alignSelf: 'flex-start', }, }) ================================================ FILE: example/app/(tabs)/index.tsx ================================================ import { StyleSheet } from 'react-native' import { Image } from 'expo-image' import { ExternalLink } from '@/components/external-link' import { HelloWave } from '@/components/hello-wave' import ParallaxScrollView from '@/components/parallax-scroll-view' import { ThemedText } from '@/components/themed-text' import { ThemedView } from '@/components/themed-view' export default function HomeScreen () { return ( } > Welcome! React Native Gifted Chat The most complete chat UI for React Native View on GitHub Tap the Explore tab to try different chat examples. ) } const styles = StyleSheet.create({ titleContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, stepContainer: { gap: 8, marginBottom: 8, }, reactLogo: { height: 178, width: 290, bottom: 0, left: 0, position: 'absolute', }, }) ================================================ FILE: example/app/_layout.tsx ================================================ import { LogBox, StyleSheet } from 'react-native' import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' import { Stack } from 'expo-router' import { StatusBar } from 'expo-status-bar' import { GestureHandlerRootView } from 'react-native-gesture-handler' import 'react-native-reanimated' import { useColorScheme } from '@/hooks/use-color-scheme' LogBox.ignoreLogs(['Open debugger to view warnings']) export const unstable_settings = { anchor: '(tabs)', } export default function RootLayout () { const colorScheme = useColorScheme() return ( ) } const styles = StyleSheet.create({ container: { flex: 1, }, }) ================================================ FILE: example/app/chat/_layout.tsx ================================================ import { TouchableOpacity, Text, StyleSheet } from 'react-native' import { Ionicons } from '@expo/vector-icons' import { Stack, useRouter } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' export default function ChatLayout () { const insets = useSafeAreaInsets() const router = useRouter() return ( ( router.back()} style={styles.backButton}> Back ), }} > ) } const styles = StyleSheet.create({ backButton: { flexDirection: 'row', alignItems: 'center', marginLeft: -8, }, backText: { color: '#007AFF', fontSize: 17, }, }) ================================================ FILE: example/app/chat/basic.tsx ================================================ import BasicExample from '@/components/chat-examples/BasicExample' export default BasicExample ================================================ FILE: example/app/chat/customized-rendering.tsx ================================================ import CustomizedRenderingExample from '@/components/chat-examples/CustomizedRenderingExample' export default CustomizedRenderingExample ================================================ FILE: example/app/chat/links.tsx ================================================ import LinksExample from '@/components/chat-examples/LinksExample' export default LinksExample ================================================ FILE: example/app/chat/reply.tsx ================================================ import ReplyExample from '@/components/chat-examples/ReplyExample' export default ReplyExample ================================================ FILE: example/app/chat/slack.tsx ================================================ import SlackExample from '@/components/chat-examples/SlackExample' export default SlackExample ================================================ FILE: example/app/modal.tsx ================================================ import { StyleSheet } from 'react-native' import { Link } from 'expo-router' import { ThemedText } from '@/components/themed-text' import { ThemedView } from '@/components/themed-view' export default function ModalScreen () { return ( This is a modal Go to home screen ) } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, link: { marginTop: 15, paddingVertical: 15, }, }) ================================================ FILE: example/app.json ================================================ { "expo": { "name": "example", "slug": "example", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "example", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { "supportsTablet": true, "bundleIdentifier": "com.anonymous.example", "config": { "googleMapsApiKey": "YOUR_GOOGLE_MAPS_API_KEY" } }, "android": { "adaptiveIcon": { "backgroundColor": "#E6F4FE", "foregroundImage": "./assets/images/android-icon-foreground.png", "backgroundImage": "./assets/images/android-icon-background.png", "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, "predictiveBackGestureEnabled": false, "package": "com.anonymous.example", "config": { "googleMaps": { "apiKey": "YOUR_GOOGLE_MAPS_API_KEY" } } }, "web": { "output": "static", "favicon": "./assets/images/favicon.png" }, "plugins": [ "expo-router", [ "expo-splash-screen", { "image": "./assets/images/splash-icon.png", "imageWidth": 200, "resizeMode": "contain", "backgroundColor": "#ffffff", "dark": { "backgroundColor": "#000000" } } ] ], "experiments": { "typedRoutes": true, "reactCompiler": true } } } ================================================ FILE: example/babel.config.js ================================================ const fs = require('fs') const path = require('path') const root = path.resolve(__dirname, '..') const rootPak = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')) module.exports = function (api) { api.cache(true) return { presets: ['babel-preset-expo'], plugins: [ [ 'module-resolver', { extensions: ['.tsx', '.ts', '.js', '.json'], alias: { // For development, we want to alias the library to the source [rootPak.name]: path.join(root, rootPak.main), }, }, ], 'react-native-worklets/plugin', ], } } ================================================ FILE: example/components/chat-examples/BasicExample.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react' import { StyleSheet, View, useColorScheme } from 'react-native' import { GiftedChat, IMessage } from 'react-native-gifted-chat' import AccessoryBar from '../../example-expo/AccessoryBar' import CustomActions from '../../example-expo/CustomActions' import CustomView from '../../example-expo/CustomView' import earlierMessages from '../../example-expo/data/earlierMessages' import messagesData from '../../example-expo/data/messages' import { useKeyboardVerticalOffset } from '../../hooks/useKeyboardVerticalOffset' import { getColorSchemeStyle } from '../../utils/styleUtils' export default function BasicExample () { const [messages, setMessages] = useState(messagesData) const [isLoadingEarlier, setIsLoadingEarlier] = useState(false) const [isTyping, setIsTyping] = useState(false) const colorScheme = useColorScheme() const keyboardVerticalOffset = useKeyboardVerticalOffset() const user = useMemo(() => ({ _id: 1, name: 'Developer', }), []) const onSend = useCallback((newMessages: IMessage[] = []) => { const messagesWithIds = newMessages.map(msg => ({ ...msg, _id: msg._id || Math.random().toString(36).substring(7), user: msg.user || user, })) setMessages(previousMessages => GiftedChat.append(previousMessages, messagesWithIds) ) }, [user]) const onPressLoadEarlierMessages = useCallback(() => { setIsLoadingEarlier(true) setTimeout(() => { setMessages(previousMessages => GiftedChat.prepend(previousMessages, earlierMessages()) ) setIsLoadingEarlier(false) }, 1500) }, []) const renderAccessory = useCallback( () => setIsTyping(isTyping => !isTyping)} user={user} />, [onSend, user] ) const renderCustomView = useCallback((props: any) => , []) const renderActions = useCallback( (props: any) => , [onSend, user] ) return ( ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, container_dark: { backgroundColor: '#000', }, messagesContainer_dark: { backgroundColor: '#000', }, composer_dark: { backgroundColor: '#1a1a1a', color: '#fff', }, }) ================================================ FILE: example/components/chat-examples/CustomizedRenderingExample.tsx ================================================ import React from 'react' import { StyleSheet, View, useColorScheme } from 'react-native' import Chats from '../../example-gifted-chat/src/Chats' import { getColorSchemeStyle } from '../../utils/styleUtils' export default function CustomizedRenderingExample () { const colorScheme = useColorScheme() return ( ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, container_dark: { backgroundColor: '#000', }, }) ================================================ FILE: example/components/chat-examples/LinksExample.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react' import { Linking, StyleSheet, Text, View } from 'react-native' import { ActionSheetProvider, useActionSheet } from '@expo/react-native-action-sheet' import { setStringAsync } from 'expo-clipboard' import { isValidPhoneNumber, parsePhoneNumberWithError } from 'libphonenumber-js' import { GiftedChat, IMessage, LinkMatcher } from 'react-native-gifted-chat' import { useKeyboardVerticalOffset } from '../../hooks/useKeyboardVerticalOffset' const LinksExample: React.FC = () => { const { showActionSheetWithOptions } = useActionSheet() const keyboardVerticalOffset = useKeyboardVerticalOffset() const initialMessages: IMessage[] = useMemo(() => [ { text: 'Welcome! 👋', createdAt: new Date(), user: { _id: 2, name: 'John Doe', }, }, { text: 'This example shows how GiftedChat handles different types of links in messages. Try tapping on any link!', createdAt: new Date(Date.now() - 1 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'Phone numbers are also parsed:\n\n• +79931234567\n\n• 89931234567\n\n• +1-215-456-7890.\n\nIt shouldn\'t parse phone numbers in file names like IMG_20220101_123456.jpg, 89201234567_today.pdf, or in addresses like 1234 React St., JS City, 56789.', createdAt: new Date(Date.now() - 2 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'Email addresses are clickable: cool.guy@example.com or contact@reactnative.dev', createdAt: new Date(Date.now() - 3 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'Different link formats work:\n• www.google.com\n• google.com\n• https://google.com', createdAt: new Date(Date.now() - 4 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'Use hashtags to categorize: #giftedchat #reactnative #opensource', createdAt: new Date(Date.now() - 5 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'You can mention people like @kesha-antonov or @john-doe', createdAt: new Date(Date.now() - 6 * 60000), user: { _id: 2, name: 'John Doe', }, }, { text: 'System message with link: Check out our documentation at https://github.com/FaridSafi/react-native-gifted-chat', createdAt: new Date(Date.now() - 7 * 60000), user: { _id: 2, name: 'John Doe', }, system: true, }, { text: 'System message with data and phone numbers: Contact support at +79931234567 or +1-215-456-7890. We have holidays on 2025-12-25 and until 2026-01-01 12:00.', createdAt: new Date(Date.now() - 7 * 60000), user: { _id: 2, name: 'John Doe', }, system: true, }, ].map((message, index) => ({ ...message, _id: index + 1, })).reverse(), []) const [messages, setMessages] = useState(initialMessages) const user = useMemo(() => ({ _id: 1, name: 'Developer', }), []) const onSend = useCallback((newMessages: IMessage[] = []) => { const messagesWithIds = newMessages.map(msg => ({ ...msg, _id: msg._id || Math.random().toString(36).substring(7), user: msg.user || user, })) setMessages(previousMessages => GiftedChat.append(previousMessages, messagesWithIds) ) }, [user]) const getValidPhoneNumber = useCallback((text: string): string | undefined => { const cleaned = text.replace(/[\-\(\)\s\.]/g, '') // Validate with libphonenumber-js try { // Try direct validation first if (isValidPhoneNumber(cleaned)) return cleaned // Try with RU region for local numbers if (isValidPhoneNumber(cleaned, 'RU')) return cleaned // Try parsing to check validity const phoneNumber = parsePhoneNumberWithError(cleaned, 'RU') if (phoneNumber && phoneNumber.isValid()) return cleaned } catch (error) { console.warn('Invalid phone number:', error) } return undefined }, []) const handlePressPhoneNumber = useCallback((url: string) => { if (!url) return // Skip if validation failed const options: { title: string action?: () => void }[] = [ { title: 'Call', action: () => Linking.openURL(`tel:${url}`) }, { title: 'Send SMS', action: () => Linking.openURL(`sms:${url}`) }, { title: 'Copy Phone Number', action: () => setStringAsync(url) }, { title: 'Cancel' }, ] showActionSheetWithOptions({ options: options.map(o => o.title), cancelButtonIndex: options.length - 1, }, (buttonIndex?: number) => { if (buttonIndex === undefined) return const option = options[buttonIndex] option.action?.() }) }, [showActionSheetWithOptions]) const matchers = useMemo(() => [ { type: 'phone', // Pattern that excludes numbers adjacent to underscores or part of filenames pattern: /(? { return getValidPhoneNumber(matchedText) || '' }, getLinkText: (text: string): string => { return text }, renderLink: (text: string, url: string, index: number) => { const validPhoneNumber = getValidPhoneNumber(text) const isDisabled = !validPhoneNumber || !url return ( !isDisabled && handlePressPhoneNumber(url)} > {text} ) }, }, ], [getValidPhoneNumber, handlePressPhoneNumber]) return ( ) } const ExampleContainer = () => ( ) export default ExampleContainer const styles = StyleSheet.create({ container: { flex: 1, }, link: { textDecorationLine: 'underline', }, linkDisabled: { textDecorationLine: 'none', }, }) ================================================ FILE: example/components/chat-examples/ReplyExample.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react' import { StyleSheet, View, useColorScheme } from 'react-native' import { GiftedChat, IMessage, ReplyMessage } from 'react-native-gifted-chat' import messagesData from '../../example-expo/data/messages' import { useKeyboardVerticalOffset } from '../../hooks/useKeyboardVerticalOffset' import { getColorSchemeStyle } from '../../utils/styleUtils' export interface IChatMessage extends IMessage { replyMessage?: ReplyMessage } export default function ReplyExample () { const [messages, setMessages] = useState(messagesData) const colorScheme = useColorScheme() const keyboardVerticalOffset = useKeyboardVerticalOffset() const user = useMemo(() => ({ _id: 1, name: 'Developer', }), []) const onSend = useCallback((newMessages: IChatMessage[] = []) => { const messagesWithIds = newMessages.map(msg => ({ ...msg, _id: msg._id || Math.random().toString(36).substring(7), user: msg.user || user, })) setMessages(previousMessages => GiftedChat.append(previousMessages, messagesWithIds) ) }, [user]) const handlePressReply = useCallback((reply: ReplyMessage) => { console.log('Pressed reply:', reply) // Could scroll to the original message here }, []) return ( messages={messages} onSend={onSend} user={user} messagesContainerStyle={getColorSchemeStyle(styles, 'messagesContainer', colorScheme)} textInputProps={{ style: getColorSchemeStyle(styles, 'composer', colorScheme), }} // New grouped reply props reply={{ swipe: { isEnabled: true, direction: 'left', // swipe left to reply }, onPress: handlePressReply, }} keyboardAvoidingViewProps={{ keyboardVerticalOffset }} /> ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, container_dark: { backgroundColor: '#000', }, messagesContainer_dark: { backgroundColor: '#000', }, composer_dark: { backgroundColor: '#1a1a1a', color: '#fff', }, }) ================================================ FILE: example/components/chat-examples/SlackExample.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react' import { StyleSheet, View, useColorScheme } from 'react-native' import { GiftedChat, IMessage } from 'react-native-gifted-chat' import messagesData from '../../example-expo/data/messages' import SlackMessage from '../../example-slack-message/src/SlackMessage' import { useKeyboardVerticalOffset } from '../../hooks/useKeyboardVerticalOffset' import { getColorSchemeStyle } from '../../utils/styleUtils' export default function SlackExample () { const [messages, setMessages] = useState(messagesData) const colorScheme = useColorScheme() const keyboardVerticalOffset = useKeyboardVerticalOffset() const user = useMemo(() => ({ _id: 1, name: 'Developer', }), []) const onSend = useCallback((newMessages: IMessage[] = []) => { const messagesWithIds = newMessages.map(msg => ({ ...msg, _id: msg._id || Math.random().toString(36).substring(7), user: msg.user || user, })) setMessages(previousMessages => GiftedChat.append(previousMessages, messagesWithIds) ) }, [user]) return ( } messagesContainerStyle={getColorSchemeStyle(styles, 'messagesContainer', colorScheme)} textInputProps={{ style: getColorSchemeStyle(styles, 'composer', colorScheme), }} keyboardAvoidingViewProps={{ keyboardVerticalOffset }} /> ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, container_dark: { backgroundColor: '#000', }, messagesContainer_dark: { backgroundColor: '#000', }, composer_dark: { backgroundColor: '#1a1a1a', color: '#fff', }, }) ================================================ FILE: example/components/external-link.tsx ================================================ import { type ComponentProps } from 'react' import { Href, Link } from 'expo-router' import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser' type Props = Omit, 'href'> & { href: Href & string } export function ExternalLink ({ href, ...rest }: Props) { return ( { console.log('ExternalLink pressed:', href) if (process.env.EXPO_OS !== 'web') { // Prevent the default behavior of linking to the default browser on native. event.preventDefault() // Open the link in an in-app browser. await openBrowserAsync(href, { presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, }) } }} /> ) } ================================================ FILE: example/components/haptic-tab.tsx ================================================ import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs' import { PlatformPressable } from '@react-navigation/elements' import * as Haptics from 'expo-haptics' export function HapticTab (props: BottomTabBarButtonProps) { return ( { if (process.env.EXPO_OS === 'ios') // Add a soft haptic feedback when pressing down on the tabs. Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) props.onPressIn?.(ev) }} /> ) } ================================================ FILE: example/components/hello-wave.tsx ================================================ import Animated from 'react-native-reanimated' export function HelloWave () { return ( 👋 ) } ================================================ FILE: example/components/parallax-scroll-view.tsx ================================================ import type { PropsWithChildren, ReactElement } from 'react' import { StyleSheet } from 'react-native' import Animated, { interpolate, useAnimatedRef, useAnimatedStyle, useScrollOffset, } from 'react-native-reanimated' import { ThemedView } from '@/components/themed-view' import { useColorScheme } from '@/hooks/use-color-scheme' import { useThemeColor } from '@/hooks/use-theme-color' const HEADER_HEIGHT = 250 type Props = PropsWithChildren<{ headerImage: ReactElement headerBackgroundColor: { dark: string, light: string } }> export default function ParallaxScrollView ({ children, headerImage, headerBackgroundColor, }: Props) { const backgroundColor = useThemeColor({}, 'background') const colorScheme = useColorScheme() ?? 'light' // eslint-disable-next-line react-hooks/exhaustive-deps const scrollRef = useAnimatedRef() const scrollOffset = useScrollOffset(scrollRef) const headerAnimatedStyle = useAnimatedStyle(() => { return { transform: [ { translateY: interpolate( scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] ), }, { scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), }, ], } }) return ( {headerImage} {children} ) } const styles = StyleSheet.create({ container: { flex: 1, }, header: { height: HEADER_HEIGHT, overflow: 'hidden', }, content: { flex: 1, padding: 32, gap: 16, overflow: 'hidden', }, }) ================================================ FILE: example/components/themed-text.tsx ================================================ import { StyleSheet, Text, type TextProps } from 'react-native' import { useThemeColor } from '@/hooks/use-theme-color' export type ThemedTextProps = TextProps & { lightColor?: string darkColor?: string type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' } export function ThemedText ({ style, lightColor, darkColor, type = 'default', ...rest }: ThemedTextProps) { const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text') return ( ) } const styles = StyleSheet.create({ default: { fontSize: 16, lineHeight: 24, }, defaultSemiBold: { fontSize: 16, lineHeight: 24, fontWeight: '600', }, title: { fontSize: 32, fontWeight: 'bold', lineHeight: 32, }, subtitle: { fontSize: 20, fontWeight: 'bold', }, link: { lineHeight: 30, fontSize: 16, color: '#0a7ea4', }, }) ================================================ FILE: example/components/themed-view.tsx ================================================ import { View, type ViewProps } from 'react-native' import { useThemeColor } from '@/hooks/use-theme-color' export type ThemedViewProps = ViewProps & { lightColor?: string darkColor?: string } export function ThemedView ({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background') return } ================================================ FILE: example/components/ui/collapsible.tsx ================================================ import { PropsWithChildren, useState } from 'react' import { StyleSheet } from 'react-native' import { RectButton } from 'react-native-gesture-handler' import { ThemedText } from '@/components/themed-text' import { ThemedView } from '@/components/themed-view' import { IconSymbol } from '@/components/ui/icon-symbol' import { Colors } from '@/constants/theme' import { useColorScheme } from '@/hooks/use-color-scheme' export function Collapsible ({ children, title }: PropsWithChildren & { title: string }) { const [isOpen, setIsOpen] = useState(false) const theme = useColorScheme() ?? 'light' return ( setIsOpen(value => !value)} > {title} {isOpen && {children}} ) } const styles = StyleSheet.create({ heading: { flexDirection: 'row', alignItems: 'center', gap: 6, }, content: { marginTop: 6, marginLeft: 24, }, }) ================================================ FILE: example/components/ui/icon-symbol.ios.tsx ================================================ import { StyleProp, ViewStyle } from 'react-native' import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols' export function IconSymbol ({ name, size = 24, color, style, weight = 'regular', }: { name: SymbolViewProps['name'] size?: number color: string style?: StyleProp weight?: SymbolWeight }) { return ( ) } ================================================ FILE: example/components/ui/icon-symbol.tsx ================================================ // Fallback for using MaterialIcons on Android and web. import { ComponentProps } from 'react' import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native' import MaterialIcons from '@expo/vector-icons/MaterialIcons' import { SymbolWeight, SymbolViewProps } from 'expo-symbols' type IconMapping = Record['name']> type IconSymbolName = keyof typeof MAPPING /** * Add your SF Symbols to Material Icons mappings here. * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. */ const MAPPING = { 'house.fill': 'home', 'paperplane.fill': 'send', 'chevron.left.forwardslash.chevron.right': 'code', 'chevron.right': 'chevron-right', } as IconMapping /** * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. * This ensures a consistent look across platforms, and optimal resource usage. * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. */ export function IconSymbol ({ name, size = 24, color, style, }: { name: IconSymbolName size?: number color: string | OpaqueColorValue style?: StyleProp weight?: SymbolWeight }) { return } ================================================ FILE: example/constants/theme.ts ================================================ /** * Below are the colors that are used in the app. The colors are defined in the light and dark mode. * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. */ import { Platform } from 'react-native' const tintColorLight = '#0a7ea4' const tintColorDark = '#fff' export const Colors = { light: { text: '#11181C', background: '#fff', tint: tintColorLight, icon: '#687076', tabIconDefault: '#687076', tabIconSelected: tintColorLight, }, dark: { text: '#ECEDEE', background: '#151718', tint: tintColorDark, icon: '#9BA1A6', tabIconDefault: '#9BA1A6', tabIconSelected: tintColorDark, }, } export const Fonts = Platform.select({ ios: { /** iOS `UIFontDescriptorSystemDesignDefault` */ sans: 'system-ui', /** iOS `UIFontDescriptorSystemDesignSerif` */ serif: 'ui-serif', /** iOS `UIFontDescriptorSystemDesignRounded` */ rounded: 'ui-rounded', /** iOS `UIFontDescriptorSystemDesignMonospaced` */ mono: 'ui-monospace', }, default: { sans: 'normal', serif: 'serif', rounded: 'normal', mono: 'monospace', }, web: { sans: 'system-ui, -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, Helvetica, Arial, sans-serif', serif: 'Georgia, \'Times New Roman\', serif', rounded: '\'SF Pro Rounded\', \'Hiragino Maru Gothic ProN\', Meiryo, \'MS PGothic\', sans-serif', mono: 'SFMono-Regular, Menlo, Monaco, Consolas, \'Liberation Mono\', \'Courier New\', monospace', }, }) ================================================ FILE: example/eslint.config.js ================================================ import stylistic from '@stylistic/eslint-plugin' import typescriptEslint from '@typescript-eslint/eslint-plugin' import typescriptParser from '@typescript-eslint/parser' import importPlugin from 'eslint-plugin-import' import jestPlugin from 'eslint-plugin-jest' import perfectionistPlugin from 'eslint-plugin-perfectionist' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' export default [ { ignores: ['**/node_modules/**', '**/lib/**', '**/build/**', '**/.expo/**', '**/android/**', '**/ios/**'], }, { files: ['**/*.{js,jsx,ts,tsx}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parser: typescriptParser, parserOptions: { ecmaFeatures: { jsx: true, }, }, globals: { fetch: 'readonly', navigator: 'readonly', __DEV__: 'readonly', XMLHttpRequest: 'readonly', FormData: 'readonly', React$Element: 'readonly', requestAnimationFrame: 'readonly', // Node.js globals for build scripts and configuration files require: 'readonly', module: 'readonly', process: 'readonly', global: 'readonly', console: 'readonly', setTimeout: 'readonly', clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', // Jest globals describe: 'readonly', test: 'readonly', it: 'readonly', jest: 'readonly', expect: 'readonly', beforeAll: 'readonly', beforeEach: 'readonly', afterAll: 'readonly', afterEach: 'readonly', }, }, plugins: { '@stylistic': stylistic, '@typescript-eslint': typescriptEslint, 'import': importPlugin, 'perfectionist': perfectionistPlugin, 'react': react, 'react-hooks': reactHooks, }, settings: { react: { version: 'detect', }, }, rules: { // React rules 'react/react-in-jsx-scope': 'off', 'react/no-unknown-property': 'off', 'react/display-name': 'off', 'react/prop-types': 'off', // React Hooks 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: '(useAnimatedStyle|useSharedValue|useAnimatedGestureHandler|useAnimatedScrollHandler|useAnimatedProps|useDerivedValue|useAnimatedRef|useAnimatedReact|useAnimatedReaction|useCallbackDebounced|useCallbackThrottled)', }, ], // TypeScript rules '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['error'], // Stylistic rules '@stylistic/semi': ['error', 'never'], '@stylistic/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none', requireLast: true, }, singleline: { delimiter: 'comma', requireLast: false, }, }, ], '@stylistic/indent': [ 'error', 2, { SwitchCase: 1, VariableDeclarator: 'first', ignoredNodes: ['TemplateLiteral'], }, ], '@stylistic/quotes': ['error', 'single'], '@stylistic/jsx-quotes': ['error', 'prefer-single'], '@stylistic/comma-dangle': [ 'error', { arrays: 'always-multiline', objects: 'always-multiline', imports: 'always-multiline', exports: 'never', functions: 'never', }, ], '@stylistic/arrow-parens': ['error', 'as-needed'], '@stylistic/template-curly-spacing': 'off', '@stylistic/linebreak-style': ['off', 'unix'], '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }], '@stylistic/jsx-closing-bracket-location': ['error', 'line-aligned'], // General rules 'no-func-assign': 'off', 'no-class-assign': 'off', 'no-useless-escape': 'off', 'no-unused-vars': 'off', // Use @typescript-eslint/no-unused-vars instead 'no-unreachable': 'error', 'curly': [2, 'multi', 'consistent'], 'nonblock-statement-body-position': ['error', 'below'], // Perfectionist rules 'perfectionist/sort-imports': [ 'error', { groups: [ 'react', 'external', 'internal', ['parent', 'sibling'], 'index', ], customGroups: { value: { react: ['^react$', '^react-native$'], }, }, newlinesBetween: 'ignore', }, ], 'perfectionist/sort-interfaces': 'off', }, }, { files: ['tests/**/*', 'src/__tests__/**/*'], plugins: { jest: jestPlugin, }, rules: { ...jestPlugin.configs.recommended.rules, }, }, ] ================================================ FILE: example/example-expo/AccessoryBar.tsx ================================================ import React from 'react' import { StyleSheet, View, useColorScheme } from 'react-native' import { MaterialIcons } from '@expo/vector-icons' import { RectButton } from 'react-native-gesture-handler' import { IMessage, User } from '../../src' import { getLocationAsync, pickImageAsync, takePictureAsync, } from './mediaUtils' export default function AccessoryBar ({ onSend, isTyping, user }: { onSend: (messages: IMessage[]) => void, isTyping: () => void, user: User }) { const colorScheme = useColorScheme() const isDark = colorScheme === 'dark' const handlePickImage = async () => { const images = await pickImageAsync() if (!images) return const messages: IMessage[] = images.map(image => ({ _id: Math.random().toString(36).substring(7), image, text: '', createdAt: new Date(), user, })) onSend(messages) } const handleTakePicture = async () => { const images = await takePictureAsync() if (!images) return const messages: IMessage[] = images.map(image => ({ _id: Math.random().toString(36).substring(7), image, text: '', createdAt: new Date(), user, })) onSend(messages) } const handleSendLocation = async () => { const location = await getLocationAsync() if (!location) return const message: IMessage = { _id: Math.random().toString(36).substring(7), location, text: '', createdAt: new Date(), user, } onSend([message]) } const containerColorStyle = colorScheme === 'dark' ? styles.container_dark : {} return (