├── .eslintignore
├── .eslintrc.cjs
├── .expo-shared
└── assets.json
├── .github
├── FUNDING.yml
├── stale.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmignore
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── babel.config.cjs
├── codecov.yml
├── example
├── .expo-shared
│ └── assets.json
├── .gitignore
├── .yarn
│ └── releases
│ │ └── yarn-classic.cjs
├── .yarnrc
├── .yarnrc.yml
├── App.tsx
├── app.json
├── assets
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── components
│ └── navbar.tsx
├── example-expo
│ ├── AccessoryBar.tsx
│ ├── CustomActions.tsx
│ ├── CustomView.tsx
│ ├── data
│ │ ├── earlierMessages.js
│ │ └── messages.js
│ └── mediaUtils.ts
├── example-gifted-chat
│ ├── README.md
│ ├── example-gifted-chat.png
│ └── src
│ │ ├── Chats.js
│ │ ├── InputToolbar.js
│ │ ├── MessageContainer.js
│ │ └── messages.js
├── example-slack-message
│ ├── README.md
│ ├── example-default-style.png
│ ├── example-slack-style.png
│ └── src
│ │ ├── SlackBubble.tsx
│ │ └── SlackMessage.tsx
├── index.js
├── ios
│ ├── .gitignore
│ ├── .xcode.env
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Podfile.properties.json
│ ├── example.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── example.xcscheme
│ ├── example.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── example
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.mm
│ │ ├── Images.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ │ ├── Info.plist
│ │ ├── PrivacyInfo.xcprivacy
│ │ ├── SplashScreen.storyboard
│ │ ├── Supporting
│ │ └── Expo.plist
│ │ └── main.m
├── metro.config.js
├── package.json
├── tsconfig.json
└── yarn.lock
├── jest.config.cjs
├── media
├── logo_sponsor.png
└── stream-logo.png
├── package.json
├── screenshots
├── gifted-chat-1.png
├── gifted-chat-2.png
├── iPhone-6s-gifted-chat-1.png
├── iPhone-6s-gifted-chat-2.png
└── iPhone-6s-gifted-chat-3.png
├── 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
├── LoadEarlier.tsx
├── Message
│ ├── index.tsx
│ ├── styles.ts
│ └── types.ts
├── MessageAudio.tsx
├── MessageContainer
│ ├── components
│ │ ├── DayAnimated
│ │ │ ├── index.tsx
│ │ │ ├── styles.ts
│ │ │ └── types.ts
│ │ └── Item
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ ├── index.tsx
│ ├── styles.ts
│ └── types.ts
├── MessageImage.tsx
├── MessageText.tsx
├── MessageVideo.tsx
├── QuickReplies.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
│ ├── GiftedAvatar.test.tsx
│ ├── GiftedChat.test.tsx
│ ├── InputToolbar.test.tsx
│ ├── LoadEarlier.test.tsx
│ ├── Message.test.tsx
│ ├── MessageContainer.test.tsx
│ ├── MessageImage.test.tsx
│ ├── MessageText.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
│ │ ├── GiftedAvatar.test.tsx.snap
│ │ ├── GiftedChat.test.tsx.snap
│ │ ├── InputToolbar.test.tsx.snap
│ │ ├── LoadEarlier.test.tsx.snap
│ │ ├── Message.test.tsx.snap
│ │ ├── MessageContainer.test.tsx.snap
│ │ ├── MessageImage.test.tsx.snap
│ │ ├── MessageText.test.tsx.snap
│ │ ├── Send.test.tsx.snap
│ │ ├── SystemMessage.test.tsx.snap
│ │ └── Time.test.tsx.snap
│ ├── data.ts
│ └── utils.test.ts
├── hooks
│ └── useUpdateLayoutEffect.ts
├── index.ts
├── logging.ts
├── styles.ts
├── types.ts
└── utils.ts
├── tests
└── setup.js
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | /lib
2 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es2021: true,
4 | // jest: true,
5 | browser: true,
6 | node: true,
7 | },
8 | parser: '@typescript-eslint/parser',
9 | extends: [
10 | 'standard',
11 | 'eslint:recommended',
12 | 'plugin:react/recommended',
13 | 'plugin:react-hooks/recommended',
14 | "plugin:@typescript-eslint/eslint-recommended",
15 | 'plugin:@typescript-eslint/recommended',
16 | 'plugin:json/recommended-legacy',
17 | 'plugin:jest/recommended',
18 | ],
19 | overrides: [
20 | {
21 | env: {
22 | node: true,
23 | },
24 | files: ['.eslintrc.{js,cjs}'],
25 | parserOptions: {
26 | sourceType: 'script',
27 | project: './tsconfig.json',
28 | },
29 | },
30 | {
31 | files: ["tests/**/*"],
32 | plugins: ["jest"],
33 | env: {
34 | 'jest/globals': true,
35 | },
36 | },
37 | ],
38 | parserOptions: {
39 | ecmaFeatures: {
40 | jsx: true,
41 | },
42 | ecmaVersion: 'latest',
43 | sourceType: 'module',
44 | },
45 | plugins: [
46 | '@stylistic',
47 | 'react',
48 | 'react-hooks',
49 | ],
50 | settings: {
51 | react: {
52 | version: 'detect',
53 | },
54 | },
55 | rules: {
56 | 'react/react-in-jsx-scope': 0,
57 | '@stylistic/no-explicit-any': 'off',
58 | 'react/no-unknown-property': 0,
59 | 'indent': [
60 | 'error',
61 | 2,
62 | {
63 | SwitchCase: 1,
64 | VariableDeclarator: 'first',
65 | ignoredNodes: ['TemplateLiteral'],
66 | },
67 | ],
68 | 'template-curly-spacing': 'off',
69 | 'linebreak-style': ['off', 'unix'],
70 | 'quotes': ['error', 'single'],
71 | 'jsx-quotes': ['error', 'prefer-single'],
72 | '@stylistic/semi': ['error', 'never'],
73 | '@stylistic/member-delimiter-style': [
74 | 'error',
75 | {
76 | multiline: {
77 | delimiter: 'none', // No semicolon for multiline
78 | requireLast: true,
79 | },
80 | singleline: {
81 | delimiter: 'comma', // Use comma for single line
82 | requireLast: false,
83 | },
84 | },
85 | ],
86 | 'comma-dangle': [
87 | 'error',
88 | {
89 | arrays: 'always-multiline',
90 | objects: 'always-multiline',
91 | imports: 'always-multiline',
92 | exports: 'never',
93 | functions: 'never',
94 | },
95 | ],
96 | 'arrow-parens': ['error', 'as-needed'],
97 | 'no-func-assign': 'off',
98 | 'no-class-assign': 'off',
99 | 'no-useless-escape': 'off',
100 | 'curly': [2, 'multi', 'consistent'],
101 | 'react/display-name': 'off',
102 | 'react-hooks/exhaustive-deps': [
103 | 'warn',
104 | {
105 | additionalHooks:
106 | '(useAnimatedStyle|useSharedValue|useAnimatedGestureHandler|useAnimatedScrollHandler|useAnimatedProps|useDerivedValue|useAnimatedRef|useAnimatedReact|useAnimatedReaction)',
107 | // useAnimatedReaction
108 | // USE RULE FUNC/FUNC/DEPS
109 | },
110 | ],
111 | 'no-unused-vars': ['error'],
112 | 'brace-style': ['error', '1tbs', { allowSingleLine: false }],
113 | 'nonblock-statement-body-position': ['error', 'below'],
114 | '@stylistic/jsx-closing-bracket-location': ['error', 'line-aligned'],
115 | 'no-unreachable': 'error',
116 | 'react/prop-types': 'off',
117 | },
118 | globals: {
119 | describe: 'readonly',
120 | test: 'readonly',
121 | jest: 'readonly',
122 | expect: 'readonly',
123 | fetch: 'readonly',
124 | navigator: 'readonly',
125 | __DEV__: 'readonly',
126 | XMLHttpRequest: 'readonly',
127 | FormData: 'readonly',
128 | React$Element: 'readonly',
129 | requestAnimationFrame: 'readonly',
130 | },
131 | }
132 |
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "5c6d215cbde93d15ae63d2ea43dfe8bf8a79a53146382cf7f3f0089bec2fc5d6": true,
3 | "d0e86e9f72936ac85597d9cd6415cf22a3c208505d5586ac04de09d4dd305707": true,
4 | "b884dbf3daca9d0a4de2f6552aa4dbdda9cbff26e2677bd76a514d71af2e7e2c": true,
5 | "30bf2d1edfc90d2841794660cf30a94bb134b89d4808bd4b05cac2304cf6fad7": true,
6 | "01d8b00b4e3d1dfab70e1ef3354b373f13bdcc3ad29122b3afacf34afd043960": true,
7 | "36cb6cfb9a281169f9ba1eb7c345fba1d56a0c7115fbf8c4b3753427aad8edaa": true,
8 | "3232d6cbd4824ece99982787e431ff1425df1d22288961602506b046c50bc516": true,
9 | "5c8d230c038116f9327c1a38157e7b5d25e1d6bbfbb0ba4e86310f097c3d0f9f": true,
10 | "250d0d32ab3051aee4b8f9d26a3299e6b3d8e6ee137dffe8a7e183e5478a2040": true
11 | }
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [faridsafi, xcarpentier, johan-dutoit, kesha-antonov]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 15
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | Sorry, but this issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. BTW Thank you
15 | for your contributions 😀 !!!
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | checks:
10 | runs-on: ubuntu-latest
11 |
12 | strategy:
13 | matrix:
14 | node-version: [18, 20]
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 |
19 | - name: Use Node.js ${{ matrix.node-version }}
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Node modules
25 | run: |
26 | yarn install
27 |
28 | - name: Lint
29 | run: |
30 | yarn build
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | .expo/
4 | npm-debug.log
5 | TODO.md
6 | .idea
7 | .vscode
8 | Exponent-*.app
9 | *.log
10 | lib/
11 | coverage/
12 | web-build/
13 | .eslintcache
14 |
15 | # Yarn
16 | .yarn/*
17 | !.yarn/patches
18 | !.yarn/plugins
19 | !.yarn/releases
20 | !.yarn/sdks
21 | !.yarn/versions
22 | yarn-error.log
23 |
24 | example_bare/vendor
25 | example_bare/**/build
26 | example_bare/ios/Pods
27 | example_bare/android/.gradle
28 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | yarn lint-staged
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .expo/
2 | .expo-shared/
3 | .circleci/
4 | .github/
5 | .vscode/
6 | example/
7 | example-expo/
8 | example-slack-message/
9 | example-gifted-chat/
10 | screenshots/
11 | babel.config.js
12 | tests/
13 | README.md
14 | ISSUE_TEMPLATE.md
15 | codecov.yml
16 | media/
17 | App.tsx
18 | app.json
19 | metro.config.js
20 | src/
21 | tsconfig.json
22 | tslint.json
23 | yarn.lock
24 | flow-typedefs/
25 | .flowconfig
26 | yarn-error.log
27 | web-build/
28 | types.d.ts
29 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### Issue Description
2 |
3 | [FILL THIS OUT]
4 |
5 | #### Steps to Reproduce / Code Snippets
6 |
7 | [FILL THIS OUT]
8 |
9 | #### Expected Results
10 |
11 | [FILL THIS OUT]
12 |
13 | #### Additional Information
14 |
15 | * Nodejs version: [FILL THIS OUT]
16 | * React version: [FILL THIS OUT]
17 | * React Native version: [FILL THIS OUT]
18 | * react-native-gifted-chat version: [FILL THIS OUT]
19 | * Platform(s) (iOS, Android, or both?): [FILL THIS OUT]
20 | * TypeScript version: [FILL THIS OUT]
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Farid from Safi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 |
4 | return {
5 | presets: [
6 | '@babel/preset-env',
7 | 'module:@react-native/babel-preset',
8 | '@babel/preset-typescript',
9 | ],
10 | plugins: [
11 | '@babel/plugin-transform-unicode-property-regex',
12 | '@babel/plugin-transform-react-jsx',
13 | 'react-native-reanimated/plugin',
14 | ],
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | patch:
4 | default: off
5 |
--------------------------------------------------------------------------------
/example/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
16 | ios/.xcode.env.local
17 | *.hprof
18 | .cxx/
19 |
20 | /vendor/bundle/
21 |
22 | # Yarn
23 | .yarn/*
24 | !.yarn/patches
25 | !.yarn/plugins
26 | !.yarn/releases
27 | !.yarn/sdks
28 | !.yarn/versions
29 | yarn-error.log
30 |
31 | # @generated expo-cli sync-8d4afeec25ea8a192358fae2f8e2fc766bdce4ec
32 | # The following patterns were generated by expo-cli
33 |
34 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
35 |
36 | # dependencies
37 | node_modules/
38 |
39 | # Expo
40 | .expo/
41 | dist/
42 | web-build/
43 | expo-env.d.ts
44 |
45 | # Native
46 | *.orig.*
47 | *.jks
48 | *.p8
49 | *.p12
50 | *.key
51 | *.mobileprovision
52 |
53 | # Metro
54 | .metro-health-check*
55 |
56 | # debug
57 | npm-debug.*
58 | yarn-debug.*
59 | yarn-error.*
60 |
61 | # macOS
62 | .DS_Store
63 | *.pem
64 |
65 | # local env files
66 | .env*.local
67 |
68 | # typescript
69 | *.tsbuildinfo
70 |
71 | # @end expo-cli
--------------------------------------------------------------------------------
/example/.yarnrc:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | yarn-path ".yarn/releases/yarn-1.22.22.cjs"
6 |
--------------------------------------------------------------------------------
/example/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-classic.cjs
4 |
--------------------------------------------------------------------------------
/example/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useReducer } from 'react'
2 | import { Alert, Linking, Platform, StyleSheet, Text, View } from 'react-native'
3 | import { MaterialIcons } from '@expo/vector-icons'
4 | import {
5 | GiftedChat,
6 | IMessage,
7 | Send,
8 | SendProps,
9 | SystemMessage,
10 | } from 'react-native-gifted-chat'
11 | import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
12 | import { NavBar } from './components/navbar'
13 | import AccessoryBar from './example-expo/AccessoryBar'
14 | import CustomActions from './example-expo/CustomActions'
15 | import CustomView from './example-expo/CustomView'
16 | import earlierMessages from './example-expo/data/earlierMessages'
17 | import messagesData from './example-expo/data/messages'
18 | import * as Clipboard from 'expo-clipboard'
19 |
20 | const user = {
21 | _id: 1,
22 | name: 'Developer',
23 | }
24 |
25 | // const otherUser = {
26 | // _id: 2,
27 | // name: 'React Native',
28 | // avatar: 'https://facebook.github.io/react/img/logo_og.png',
29 | // }
30 |
31 | interface IState {
32 | messages: any[]
33 | step: number
34 | loadEarlier?: boolean
35 | isLoadingEarlier?: boolean
36 | isTyping: boolean
37 | }
38 |
39 | enum ActionKind {
40 | SEND_MESSAGE = 'SEND_MESSAGE',
41 | LOAD_EARLIER_MESSAGES = 'LOAD_EARLIER_MESSAGES',
42 | LOAD_EARLIER_START = 'LOAD_EARLIER_START',
43 | SET_IS_TYPING = 'SET_IS_TYPING',
44 | // LOAD_EARLIER_END = 'LOAD_EARLIER_END',
45 | }
46 |
47 | // An interface for our actions
48 | interface StateAction {
49 | type: ActionKind
50 | payload?: any
51 | }
52 |
53 | function reducer (state: IState, action: StateAction) {
54 | switch (action.type) {
55 | case ActionKind.SEND_MESSAGE: {
56 | return {
57 | ...state,
58 | step: state.step + 1,
59 | messages: action.payload,
60 | }
61 | }
62 | case ActionKind.LOAD_EARLIER_MESSAGES: {
63 | return {
64 | ...state,
65 | loadEarlier: true,
66 | isLoadingEarlier: false,
67 | messages: action.payload,
68 | }
69 | }
70 | case ActionKind.LOAD_EARLIER_START: {
71 | return {
72 | ...state,
73 | isLoadingEarlier: true,
74 | }
75 | }
76 | case ActionKind.SET_IS_TYPING: {
77 | return {
78 | ...state,
79 | isTyping: action.payload,
80 | }
81 | }
82 | }
83 | }
84 |
85 | const App = () => {
86 | const [state, dispatch] = useReducer(reducer, {
87 | messages: messagesData,
88 | step: 0,
89 | loadEarlier: true,
90 | isLoadingEarlier: false,
91 | isTyping: false,
92 | })
93 |
94 | const onSend = useCallback(
95 | (messages: any[]) => {
96 | const sentMessages = [{ ...messages[0], sent: true, received: true }]
97 | const newMessages = GiftedChat.append(
98 | state.messages,
99 | sentMessages,
100 | Platform.OS !== 'web'
101 | )
102 |
103 | dispatch({ type: ActionKind.SEND_MESSAGE, payload: newMessages })
104 | },
105 | [dispatch, state.messages]
106 | )
107 |
108 | const onLoadEarlier = useCallback(() => {
109 | dispatch({ type: ActionKind.LOAD_EARLIER_START })
110 | setTimeout(() => {
111 | const newMessages = GiftedChat.prepend(
112 | state.messages,
113 | earlierMessages() as IMessage[],
114 | Platform.OS !== 'web'
115 | )
116 |
117 | dispatch({ type: ActionKind.LOAD_EARLIER_MESSAGES, payload: newMessages })
118 | }, 1500) // simulating network
119 | // }, 15000) // for debug with long loading
120 | }, [dispatch, state.messages])
121 |
122 | const parsePatterns = useCallback(() => {
123 | return [
124 | {
125 | pattern: /#(\w+)/,
126 | style: { textDecorationLine: 'underline', color: 'darkorange' },
127 | onPress: () => Linking.openURL('http://gifted.chat'),
128 | },
129 | ]
130 | }, [])
131 |
132 | const onLongPressAvatar = useCallback((pressedUser: any) => {
133 | Alert.alert(JSON.stringify(pressedUser))
134 | }, [])
135 |
136 | const onPressAvatar = useCallback(() => {
137 | Alert.alert('On avatar press')
138 | }, [])
139 |
140 | const handleLongPress = useCallback((context: unknown, currentMessage: object) => {
141 | if (!currentMessage.text)
142 | return
143 |
144 | const options = [
145 | 'Copy text',
146 | 'Cancel',
147 | ]
148 |
149 | const cancelButtonIndex = options.length - 1
150 |
151 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
152 | ;(context as any).actionSheet().showActionSheetWithOptions(
153 | {
154 | options,
155 | cancelButtonIndex,
156 | },
157 | (buttonIndex: number) => {
158 | switch (buttonIndex) {
159 | case 0:
160 | Clipboard.setStringAsync(currentMessage.text)
161 | break
162 | default:
163 | break
164 | }
165 | }
166 | )
167 | }, [])
168 |
169 | const onQuickReply = useCallback((replies: any[]) => {
170 | const createdAt = new Date()
171 | if (replies.length === 1)
172 | onSend([
173 | {
174 | createdAt,
175 | _id: Math.round(Math.random() * 1000000),
176 | text: replies[0].title,
177 | user,
178 | },
179 | ])
180 | else if (replies.length > 1)
181 | onSend([
182 | {
183 | createdAt,
184 | _id: Math.round(Math.random() * 1000000),
185 | text: replies.map(reply => reply.title).join(', '),
186 | user,
187 | },
188 | ])
189 | else
190 | console.warn('replies param is not set correctly')
191 | }, [])
192 |
193 | const renderQuickReplySend = useCallback(() => {
194 | return {' custom send =>'}
195 | }, [])
196 |
197 | const setIsTyping = useCallback(
198 | (isTyping: boolean) => {
199 | dispatch({ type: ActionKind.SET_IS_TYPING, payload: isTyping })
200 | },
201 | [dispatch]
202 | )
203 |
204 | const onSendFromUser = useCallback(
205 | (messages: IMessage[] = []) => {
206 | const createdAt = new Date()
207 | const messagesToUpload = messages.map(message => ({
208 | ...message,
209 | user,
210 | createdAt,
211 | _id: Math.round(Math.random() * 1000000),
212 | }))
213 |
214 | onSend(messagesToUpload)
215 | },
216 | [onSend]
217 | )
218 |
219 | const renderAccessory = useCallback(() => {
220 | return (
221 | setIsTyping(!state.isTyping)}
224 | />
225 | )
226 | }, [onSendFromUser, setIsTyping, state.isTyping])
227 |
228 | const renderCustomActions = useCallback(
229 | props =>
230 | Platform.OS === 'web'
231 | ? null
232 | : (
233 |
234 | ),
235 | [onSendFromUser]
236 | )
237 |
238 | const renderSystemMessage = useCallback(props => {
239 | return (
240 |
249 | )
250 | }, [])
251 |
252 | const renderCustomView = useCallback(props => {
253 | return
254 | }, [])
255 |
256 | const renderSend = useCallback((props: SendProps) => {
257 | return (
258 |
259 |
260 |
261 | )
262 | }, [])
263 |
264 | const insets = useSafeAreaInsets()
265 |
266 | return (
267 |
268 |
269 |
270 |
303 |
304 |
305 | )
306 | }
307 |
308 | const AppWrapper = () => {
309 | return (
310 |
311 |
312 |
313 | )
314 | }
315 |
316 | const styles = StyleSheet.create({
317 | fill: {
318 | flex: 1,
319 | },
320 | container: {
321 | backgroundColor: '#f5f5f5',
322 | },
323 | content: {
324 | backgroundColor: '#ffffff',
325 | },
326 | })
327 |
328 | export default AppWrapper
329 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "splash": {
9 | "image": "./assets/splash.png",
10 | "resizeMode": "contain",
11 | "backgroundColor": "#ffffff"
12 | },
13 | "updates": {
14 | "fallbackToCacheTimeout": 0
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true,
21 | "bundleIdentifier": "org.name.example"
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/adaptive-icon.png",
26 | "backgroundColor": "#FFFFFF"
27 | }
28 | },
29 | "web": {
30 | "favicon": "./assets/favicon.png"
31 | },
32 | "plugins": [
33 | [
34 | "expo-build-properties",
35 | {
36 | "android": {
37 | "kotlinVersion": "1.6.21"
38 | }
39 | }
40 | ]
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/assets/splash.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = function (api) {
4 | api.cache(true)
5 |
6 | return {
7 | presets: ['babel-preset-expo'],
8 | plugins: [
9 | [
10 | 'module-resolver',
11 | {
12 | resolvePath: (sourcePath, currentFile, opts) => {
13 | if (/react\-native\-gifted\-chat/ig.test(sourcePath)) {
14 | let relativePath = new Array(currentFile.replace(path.join(__dirname, '../'), '').split('/').length - 1).fill('..').join('/')
15 | relativePath = path.join(relativePath, 'src', sourcePath.replace(/react\-native\-gifted\-chat(?:\/src)?/ig, ''))
16 | return relativePath
17 | }
18 |
19 | return sourcePath
20 | },
21 | },
22 | ],
23 | 'react-native-reanimated/plugin',
24 | ],
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/example/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text, Platform } from 'react-native'
3 |
4 | export function NavBar () {
5 | if (Platform.OS === 'web')
6 | return null
7 |
8 | return (
9 |
15 | 💬 Gifted Chat{'\n'}
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/example/example-expo/AccessoryBar.tsx:
--------------------------------------------------------------------------------
1 | import { MaterialIcons } from '@expo/vector-icons'
2 | import React from 'react'
3 | import { StyleSheet, TouchableOpacity, View } from 'react-native'
4 |
5 | import {
6 | getLocationAsync,
7 | pickImageAsync,
8 | takePictureAsync,
9 | } from './mediaUtils'
10 |
11 | export default class AccessoryBar extends React.Component {
12 | render () {
13 | const { onSend, isTyping } = this.props
14 |
15 | return (
16 |
17 |
27 | )
28 | }
29 | }
30 |
31 | const Button = ({
32 | onPress,
33 | size = 30,
34 | color = 'rgba(0,0,0,0.5)',
35 | ...props
36 | }) => (
37 |
38 |
39 |
40 | )
41 |
42 | const styles = StyleSheet.create({
43 | container: {
44 | height: 44,
45 | width: '100%',
46 | backgroundColor: 'white',
47 | flexDirection: 'row',
48 | justifyContent: 'space-around',
49 | alignItems: 'center',
50 | borderTopWidth: StyleSheet.hairlineWidth,
51 | borderTopColor: 'rgba(0,0,0,0.3)',
52 | },
53 | })
54 |
--------------------------------------------------------------------------------
/example/example-expo/CustomActions.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import {
3 | StyleProp,
4 | ViewStyle,
5 | TextStyle,
6 | StyleSheet,
7 | Text,
8 | TouchableOpacity,
9 | View,
10 | } from 'react-native'
11 |
12 | import { useActionSheet } from '@expo/react-native-action-sheet'
13 | import {
14 | getLocationAsync,
15 | pickImageAsync,
16 | takePictureAsync,
17 | } from './mediaUtils'
18 |
19 | interface Props {
20 | renderIcon?: () => React.ReactNode
21 | wrapperStyle?: StyleProp
22 | containerStyle?: StyleProp
23 | iconTextStyle?: StyleProp
24 | onSend: (messages: unknown) => void
25 | }
26 |
27 | const CustomActions = ({
28 | renderIcon,
29 | iconTextStyle,
30 | containerStyle,
31 | wrapperStyle,
32 | onSend,
33 | }: Props) => {
34 | const { showActionSheetWithOptions } = useActionSheet()
35 |
36 | const onActionsPress = useCallback(() => {
37 | const options = [
38 | 'Choose From Library',
39 | 'Take Picture',
40 | 'Send Location',
41 | 'Cancel',
42 | ]
43 | const cancelButtonIndex = options.length - 1
44 |
45 | showActionSheetWithOptions(
46 | {
47 | options,
48 | cancelButtonIndex,
49 | },
50 | async buttonIndex => {
51 | switch (buttonIndex) {
52 | case 0:
53 | pickImageAsync(onSend)
54 | return
55 | case 1:
56 | takePictureAsync(onSend)
57 | return
58 | case 2:
59 | getLocationAsync(onSend)
60 | }
61 | }
62 | )
63 | }, [showActionSheetWithOptions, onSend])
64 |
65 | const renderIconComponent = useCallback(() => {
66 | if (renderIcon)
67 | return renderIcon()
68 |
69 | return (
70 |
71 | +
72 |
73 | )
74 | }, [renderIcon, wrapperStyle, iconTextStyle])
75 |
76 | return (
77 |
81 | {renderIconComponent()}
82 |
83 | )
84 | }
85 |
86 | export default CustomActions
87 |
88 | const styles = StyleSheet.create({
89 | container: {
90 | width: 26,
91 | height: 26,
92 | marginLeft: 10,
93 | marginBottom: 10,
94 | },
95 | wrapper: {
96 | borderRadius: 13,
97 | borderColor: '#b2b2b2',
98 | borderWidth: 2,
99 | flex: 1,
100 | alignItems: 'center',
101 | justifyContent: 'center',
102 | },
103 | iconText: {
104 | color: '#b2b2b2',
105 | fontWeight: 'bold',
106 | fontSize: 16,
107 | lineHeight: 16,
108 | backgroundColor: 'transparent',
109 | textAlign: 'center',
110 | },
111 | })
112 |
--------------------------------------------------------------------------------
/example/example-expo/CustomView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import * as Linking from 'expo-linking'
3 | import {
4 | Platform,
5 | StyleSheet,
6 | TouchableOpacity,
7 | View,
8 | Text,
9 | StyleProp,
10 | ViewStyle,
11 | } from 'react-native'
12 | import MapView from 'react-native-maps'
13 | // import { ProgressBar } from 'react-native-paper'
14 |
15 | interface Props {
16 | currentMessage: any
17 | containerStyle?: StyleProp
18 | mapViewStyle?: StyleProp
19 | }
20 |
21 | const CustomView = ({
22 | currentMessage,
23 | containerStyle,
24 | mapViewStyle,
25 | }: Props) => {
26 | const openMapAsync = useCallback(async () => {
27 | if (Platform.OS === 'web') {
28 | alert('Opening the map is not supported.')
29 | return
30 | }
31 |
32 | const { location = {} } = currentMessage
33 |
34 | const url = Platform.select({
35 | ios: `http://maps.apple.com/?ll=${location.latitude},${location.longitude}`,
36 | default: `http://maps.google.com/?q=${location.latitude},${location.longitude}`,
37 | })
38 |
39 | try {
40 | const supported = await Linking.canOpenURL(url)
41 | if (supported)
42 | return Linking.openURL(url)
43 |
44 | alert('Opening the map is not supported.')
45 | } catch (e) {
46 | alert(e.message)
47 | }
48 | }, [currentMessage])
49 |
50 | // left this here for testing re-rendering of messages on send
51 | // return (
52 | //
53 | // )
54 |
55 | if (currentMessage.location)
56 | return (
57 |
61 | {Platform.OS !== 'web'
62 | ? (
63 |
74 | )
75 | : (
76 |
77 |
78 | Map not supported in web yet, sorry!
79 |
80 |
81 | )}
82 |
83 | )
84 |
85 | return null
86 | }
87 |
88 | export default CustomView
89 |
90 | const styles = StyleSheet.create({
91 | container: {},
92 | mapView: {
93 | width: 150,
94 | height: 100,
95 | borderRadius: 13,
96 | margin: 3,
97 | },
98 | })
99 |
--------------------------------------------------------------------------------
/example/example-expo/data/earlierMessages.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | const date = dayjs().subtract(1, 'year')
4 |
5 | export default () => [
6 | {
7 | _id: Math.round(Math.random() * 1000000),
8 | text:
9 | 'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
10 | createdAt: date,
11 | user: {
12 | _id: 1,
13 | name: 'Developer',
14 | },
15 | },
16 | {
17 | _id: Math.round(Math.random() * 1000000),
18 | text:
19 | 'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
20 | createdAt: date,
21 | user: {
22 | _id: 1,
23 | name: 'Developer',
24 | },
25 | },
26 | {
27 | _id: Math.round(Math.random() * 1000000),
28 | text:
29 | 'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
30 | createdAt: date,
31 | user: {
32 | _id: 1,
33 | name: 'Developer',
34 | },
35 | },
36 | {
37 | _id: Math.round(Math.random() * 1000000),
38 | text:
39 | 'It uses the same design as React, letting you compose a rich mobile UI from declarative components https://facebook.github.io/react-native/',
40 | createdAt: date,
41 | user: {
42 | _id: 1,
43 | name: 'Developer',
44 | },
45 | },
46 | {
47 | _id: Math.round(Math.random() * 1000000),
48 | text: 'React Native lets you build mobile apps using only JavaScript',
49 | createdAt: date,
50 | user: {
51 | _id: 1,
52 | name: 'Developer',
53 | },
54 | },
55 | {
56 | _id: Math.round(Math.random() * 1000000),
57 | text: 'React Native lets you build mobile apps using only JavaScript',
58 | createdAt: date,
59 | user: {
60 | _id: 1,
61 | name: 'Developer',
62 | },
63 | },
64 | {
65 | _id: Math.round(Math.random() * 1000000),
66 | text: 'React Native lets you build mobile apps using only JavaScript',
67 | createdAt: date,
68 | user: {
69 | _id: 1,
70 | name: 'Developer',
71 | },
72 | },
73 | {
74 | _id: Math.round(Math.random() * 1000000),
75 | text: 'React Native lets you build mobile apps using only JavaScript',
76 | createdAt: date,
77 | user: {
78 | _id: 1,
79 | name: 'Developer',
80 | },
81 | },
82 | {
83 | _id: Math.round(Math.random() * 1000000),
84 | text: 'React Native lets you build mobile apps using only JavaScript',
85 | createdAt: date,
86 | user: {
87 | _id: 1,
88 | name: 'Developer',
89 | },
90 | },
91 | {
92 | _id: Math.round(Math.random() * 1000000),
93 | text: 'React Native lets you build mobile apps using only JavaScript',
94 | createdAt: date,
95 | user: {
96 | _id: 1,
97 | name: 'Developer',
98 | },
99 | },
100 | {
101 | _id: Math.round(Math.random() * 1000000),
102 | text: 'React Native lets you build mobile apps using only JavaScript',
103 | createdAt: date,
104 | user: {
105 | _id: 1,
106 | name: 'Developer',
107 | },
108 | },
109 | {
110 | _id: Math.round(Math.random() * 1000000),
111 | text: 'React Native lets you build mobile apps using only JavaScript',
112 | createdAt: date,
113 | user: {
114 | _id: 1,
115 | name: 'Developer',
116 | },
117 | },
118 | {
119 | _id: Math.round(Math.random() * 1000000),
120 | text: 'React Native lets you build mobile apps using only JavaScript',
121 | createdAt: date,
122 | user: {
123 | _id: 1,
124 | name: 'Developer',
125 | },
126 | },
127 | {
128 | _id: Math.round(Math.random() * 1000000),
129 | text: 'This is a system message.',
130 | createdAt: date,
131 | system: true,
132 | },
133 | ]
134 |
--------------------------------------------------------------------------------
/example/example-expo/data/messages.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | const date1 = dayjs()
4 | const date2 = date1.clone().subtract(1, 'day')
5 | const date3 = date2.clone().subtract(1, 'week')
6 |
7 | export default [
8 | {
9 | _id: 9,
10 | text: '#awesome 3',
11 | createdAt: date1,
12 | user: {
13 | _id: 1,
14 | name: 'Developer',
15 | },
16 | },
17 | {
18 | _id: 8,
19 | text: '#awesome 2',
20 | createdAt: date1,
21 | user: {
22 | _id: 1,
23 | name: 'Developer',
24 | },
25 | },
26 | {
27 | _id: 7,
28 | text: '#awesome',
29 | createdAt: date1,
30 | user: {
31 | _id: 1,
32 | name: 'Developer',
33 | },
34 | },
35 | {
36 | _id: 6,
37 | text: 'Paris',
38 | createdAt: date2,
39 | user: {
40 | _id: 2,
41 | name: 'React Native',
42 | },
43 | image:
44 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Paris_-_Eiffelturm_und_Marsfeld2.jpg/280px-Paris_-_Eiffelturm_und_Marsfeld2.jpg',
45 | sent: true,
46 | received: true,
47 | },
48 | {
49 | _id: 5,
50 | text: 'Send me a picture!',
51 | createdAt: date2,
52 | user: {
53 | _id: 1,
54 | name: 'Developer',
55 | },
56 | },
57 | {
58 | _id: 4,
59 | text: '',
60 | createdAt: date2,
61 | user: {
62 | _id: 2,
63 | name: 'React Native',
64 | },
65 | sent: true,
66 | received: true,
67 | location: {
68 | latitude: 48.864601,
69 | longitude: 2.398704,
70 | },
71 | },
72 | {
73 | _id: 3,
74 | text: 'Where are you?',
75 | createdAt: date3,
76 | user: {
77 | _id: 1,
78 | name: 'Developer',
79 | },
80 | },
81 | {
82 | _id: 2,
83 | text: 'Yes, and I use #GiftedChat!',
84 | createdAt: date3,
85 | user: {
86 | _id: 2,
87 | name: 'React Native',
88 | },
89 | sent: true,
90 | received: true,
91 | },
92 | {
93 | _id: 1,
94 | text: 'Are you building a chat app?',
95 | createdAt: date3,
96 | user: {
97 | _id: 1,
98 | name: 'Developer',
99 | },
100 | },
101 | {
102 | _id: 10,
103 | text: 'This is a quick reply. Do you love Gifted Chat? (radio) KEEP IT',
104 | createdAt: date3,
105 | quickReplies: {
106 | type: 'radio', // or 'checkbox',
107 | keepIt: true,
108 | values: [
109 | {
110 | title: '😋 Yes',
111 | value: 'yes',
112 | },
113 | {
114 | title:
115 | '📷 Yes, let me show you with a picture! Again let me show you with a picture!',
116 | value: 'yes_picture',
117 | },
118 | {
119 | title: '😞 Nope. What?',
120 | value: 'no',
121 | },
122 | ],
123 | },
124 | user: {
125 | _id: 2,
126 | name: 'React Native',
127 | },
128 | },
129 | {
130 | _id: 20,
131 | text: 'This is a quick reply. Do you love Gifted Chat? (checkbox)',
132 | createdAt: date3,
133 | quickReplies: {
134 | type: 'checkbox', // or 'checkbox',
135 | values: [
136 | {
137 | title: 'Yes',
138 | value: 'yes',
139 | },
140 | {
141 | title: 'Yes, let me show you with a picture!',
142 | value: 'yes_picture',
143 | },
144 | {
145 | title: 'Nope. What?',
146 | value: 'no',
147 | },
148 | ],
149 | },
150 | user: {
151 | _id: 2,
152 | name: 'React Native',
153 | },
154 | },
155 | {
156 | _id: 30,
157 | createdAt: date3,
158 | video: 'https://media.giphy.com/media/3o6ZthZjk09Xx4ktZ6/giphy.mp4',
159 | user: {
160 | _id: 2,
161 | name: 'React Native',
162 | },
163 | },
164 | {
165 | _id: 31,
166 | createdAt: date3,
167 | audio:
168 | 'https://file-examples.com/wp-content/uploads/2017/11/file_example_MP3_700KB.mp3',
169 | user: {
170 | _id: 2,
171 | name: 'React Native',
172 | },
173 | },
174 | ]
175 |
--------------------------------------------------------------------------------
/example/example-expo/mediaUtils.ts:
--------------------------------------------------------------------------------
1 | import * as Linking from 'expo-linking'
2 | import * as Location from 'expo-location'
3 | import * as Permissions from 'expo-permissions'
4 | import * as ImagePicker from 'expo-image-picker'
5 |
6 | import { Alert } from 'react-native'
7 |
8 | export default async function getPermissionAsync (
9 | permission: Permissions.PermissionType
10 | ) {
11 | const { status } = await Permissions.askAsync(permission)
12 | if (status !== 'granted') {
13 | const permissionName = permission.toLowerCase().replace('_', ' ')
14 | Alert.alert(
15 | 'Cannot be done 😞',
16 | `If you would like to use this feature, you'll need to enable the ${permissionName} permission in your phone settings.`,
17 | [
18 | {
19 | text: 'Let\'s go!',
20 | onPress: () => Linking.openURL('app-settings:'),
21 | },
22 | { text: 'Nevermind', onPress: () => {}, style: 'cancel' },
23 | ],
24 | { cancelable: true }
25 | )
26 |
27 | return false
28 | }
29 | return true
30 | }
31 |
32 | export async function getLocationAsync (
33 | onSend: (locations: { location: Location.LocationObjectCoords }[]) => void
34 | ) {
35 | const response = await Location.requestForegroundPermissionsAsync()
36 | if (!response.granted)
37 | return
38 |
39 | const location = await Location.getCurrentPositionAsync()
40 | if (!location)
41 | return
42 |
43 | onSend([{ location: location.coords }])
44 | }
45 |
46 | export async function pickImageAsync (
47 | onSend: (images: { image: string }[]) => void
48 | ) {
49 | const response = await ImagePicker.requestMediaLibraryPermissionsAsync()
50 | if (!response.granted)
51 | return
52 |
53 | const result = await ImagePicker.launchImageLibraryAsync({
54 | allowsEditing: true,
55 | aspect: [4, 3],
56 | })
57 |
58 | if (result.canceled)
59 | return
60 |
61 | const images = result.assets.map(({ uri: image }) => ({ image }))
62 | onSend(images)
63 | }
64 |
65 | export async function takePictureAsync (
66 | onSend: (images: { image: string }[]) => void
67 | ) {
68 | const response = await ImagePicker.requestCameraPermissionsAsync()
69 | if (!response.granted)
70 | return
71 |
72 | const result = await ImagePicker.launchCameraAsync({
73 | allowsEditing: true,
74 | aspect: [4, 3],
75 | })
76 |
77 | if (result.canceled)
78 | return
79 |
80 | const images = result.assets.map(({ uri: image }) => ({ image }))
81 | onSend(images)
82 | }
83 |
--------------------------------------------------------------------------------
/example/example-gifted-chat/README.md:
--------------------------------------------------------------------------------
1 | # example-gifted-chat
2 |
3 | Lots of people using `react-native-gifted-chat` might want to know that...
4 |
5 | 1. There are so many render props could use, but what should I pass?
6 | 2. How could I customize each component, but leaving its functionality.
7 |
8 | > For example, said that you want to customize the `send button`, when you pass your own component to `renderSend`, after that you will lose the functionality of clean up text when a message has been sent.
9 |
10 | ---
11 |
12 | I made this for anyone who wants to know how to use the render Props properly.
13 |
14 | ##### Such as:
15 |
16 | - renderInputToolbar
17 | - renderActions
18 | - renderComposer
19 | - renderSend
20 | - renderAvatar
21 | - renderBubble
22 | - renderSystemMessage
23 | - renderMessage
24 | - renderMessageText
25 | - renderCustomView
26 |
27 |
--------------------------------------------------------------------------------
/example/example-gifted-chat/example-gifted-chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/example-gifted-chat/example-gifted-chat.png
--------------------------------------------------------------------------------
/example/example-gifted-chat/src/Chats.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { GiftedChat } from 'react-native-gifted-chat'
3 | import initialMessages from './messages'
4 | import { renderInputToolbar, renderActions, renderComposer, renderSend } from './InputToolbar'
5 | import {
6 | renderAvatar,
7 | renderBubble,
8 | renderSystemMessage,
9 | renderMessage,
10 | renderMessageText,
11 | renderCustomView,
12 | } from './MessageContainer'
13 |
14 | const Chats = () => {
15 | const [text, setText] = useState('')
16 | const [messages, setMessages] = useState([])
17 |
18 | useEffect(() => {
19 | setMessages(initialMessages.reverse())
20 | }, [])
21 |
22 | const onSend = (newMessages = []) => {
23 | setMessages(prevMessages => GiftedChat.append(prevMessages, newMessages))
24 | }
25 |
26 | return (
27 | [
58 | {
59 | pattern: /#(\w+)/,
60 | style: linkStyle,
61 | onPress: tag => console.log(`Pressed on hashtag: ${tag}`),
62 | },
63 | ]}
64 | />
65 | )
66 | }
67 |
68 | export default Chats
69 |
--------------------------------------------------------------------------------
/example/example-gifted-chat/src/InputToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Image } from 'react-native'
3 | import { InputToolbar, Actions, Composer, Send } from 'react-native-gifted-chat'
4 |
5 | export const renderInputToolbar = props => (
6 |
14 | )
15 |
16 | export const renderActions = props => (
17 | (
29 |
35 | )}
36 | options={{
37 | 'Choose From Library': () => {
38 | console.log('Choose From Library')
39 | },
40 | Cancel: () => {
41 | console.log('Cancel')
42 | },
43 | }}
44 | optionTintColor="#222B45"
45 | />
46 | )
47 |
48 | export const renderComposer = props => (
49 |
62 | )
63 |
64 | export const renderSend = props => (
65 |
76 |
82 |
83 | )
84 |
--------------------------------------------------------------------------------
/example/example-gifted-chat/src/MessageContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { View, Text } from 'react-native'
3 | import { Avatar, Bubble, SystemMessage, Message, MessageText } from 'react-native-gifted-chat'
4 |
5 | export const renderAvatar = props => (
6 |
11 | )
12 |
13 | export const renderBubble = props => (
14 | Time}
17 | // renderTicks={() => Ticks}
18 | containerStyle={{
19 | left: { borderColor: 'teal', borderWidth: 8 },
20 | right: {},
21 | }}
22 | wrapperStyle={{
23 | left: { borderColor: 'tomato', borderWidth: 4 },
24 | right: {},
25 | }}
26 | bottomContainerStyle={{
27 | left: { borderColor: 'purple', borderWidth: 4 },
28 | right: {},
29 | }}
30 | tickStyle={{}}
31 | usernameStyle={{ color: 'tomato', fontWeight: '100' }}
32 | containerToNextStyle={{
33 | left: { borderColor: 'navy', borderWidth: 4 },
34 | right: {},
35 | }}
36 | containerToPreviousStyle={{
37 | left: { borderColor: 'mediumorchid', borderWidth: 4 },
38 | right: {},
39 | }}
40 | />
41 | )
42 |
43 | export const renderSystemMessage = props => (
44 |
50 | )
51 |
52 | export const renderMessage = props => (
53 | Date}
56 | containerStyle={{
57 | left: { backgroundColor: 'lime' },
58 | right: { backgroundColor: 'gold' },
59 | }}
60 | />
61 | )
62 |
63 | export const renderMessageText = props => (
64 |
80 | )
81 |
82 | export const renderCustomView = ({ user }) => (
83 |
84 |
85 | Current user:
86 | {user.name}
87 |
88 | From CustomView
89 |
90 | )
91 |
--------------------------------------------------------------------------------
/example/example-gifted-chat/src/messages.js:
--------------------------------------------------------------------------------
1 | const messages = [
2 | {
3 | _id: 1,
4 | text: 'This is a system message',
5 | createdAt: new Date(Date.UTC(2016, 5, 11, 17, 20, 0)),
6 | system: true,
7 | },
8 | {
9 | _id: 2,
10 | text: 'Hello developer',
11 | createdAt: new Date(Date.UTC(2016, 5, 12, 17, 20, 0)),
12 | user: {
13 | _id: 2,
14 | name: 'React Native',
15 | avatar: 'https://placeimg.com/140/140/any',
16 | },
17 | },
18 | {
19 | _id: 3,
20 | text: 'Hi! I work from home today!',
21 | createdAt: new Date(Date.UTC(2016, 5, 13, 17, 20, 0)),
22 | user: {
23 | _id: 1,
24 | name: 'React Native',
25 | avatar: 'https://placeimg.com/140/140/any',
26 | },
27 | image: 'https://placeimg.com/960/540/any',
28 | },
29 | {
30 | _id: 4,
31 | text: 'This is a quick reply. Do you love Gifted Chat? (radio) KEEP IT',
32 | createdAt: new Date(Date.UTC(2016, 5, 14, 17, 20, 0)),
33 | user: {
34 | _id: 2,
35 | name: 'React Native',
36 | avatar: 'https://placeimg.com/140/140/any',
37 | },
38 | quickReplies: {
39 | type: 'radio', // or 'checkbox',
40 | keepIt: true,
41 | values: [
42 | {
43 | title: '😋 Yes',
44 | value: 'yes',
45 | },
46 | {
47 | title: '📷 Yes, let me show you with a picture!',
48 | value: 'yes_picture',
49 | },
50 | {
51 | title: '😞 Nope. What?',
52 | value: 'no',
53 | },
54 | ],
55 | },
56 | },
57 | {
58 | _id: 5,
59 | text: 'This is a quick reply. Do you love Gifted Chat? (checkbox)',
60 | createdAt: new Date(Date.UTC(2016, 5, 15, 17, 20, 0)),
61 | user: {
62 | _id: 2,
63 | name: 'React Native',
64 | avatar: 'https://placeimg.com/140/140/any',
65 | },
66 | quickReplies: {
67 | type: 'checkbox', // or 'radio',
68 | values: [
69 | {
70 | title: 'Yes',
71 | value: 'yes',
72 | },
73 | {
74 | title: 'Yes, let me show you with a picture!',
75 | value: 'yes_picture',
76 | },
77 | {
78 | title: 'Nope. What?',
79 | value: 'no',
80 | },
81 | ],
82 | },
83 | },
84 | {
85 | _id: 6,
86 | text: 'Come on!',
87 | createdAt: new Date(Date.UTC(2016, 5, 15, 18, 20, 0)),
88 | user: {
89 | _id: 2,
90 | name: 'React Native',
91 | avatar: 'https://placeimg.com/140/140/any',
92 | },
93 | },
94 | {
95 | _id: 7,
96 | text: `Hello this is an example of the ParsedText, links like http://www.google.com or http://www.facebook.com are clickable and phone number 444-555-6666 can call too.
97 | But you can also do more with this package, for example Bob will change style and David too. foo@gmail.com
98 | And the magic number is 42!
99 | #react #react-native`,
100 | createdAt: new Date(Date.UTC(2016, 5, 13, 17, 20, 0)),
101 | user: {
102 | _id: 1,
103 | name: 'React Native',
104 | avatar: 'https://placeimg.com/140/140/any',
105 | },
106 | },
107 | ]
108 |
109 | export default messages
110 |
--------------------------------------------------------------------------------
/example/example-slack-message/README.md:
--------------------------------------------------------------------------------
1 | # "Slack" style UI example
2 |
3 | Credit and inspiration comes from [Slack](https://slack.com/).
4 |
5 | Screenshots to compare:
6 |
7 | | Default style | "Slack" style |
8 | |:-------------:|:-------------:|
9 | |
|
|
10 |
--------------------------------------------------------------------------------
/example/example-slack-message/example-default-style.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/example-slack-message/example-default-style.png
--------------------------------------------------------------------------------
/example/example-slack-message/example-slack-style.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/example/example-slack-message/example-slack-style.png
--------------------------------------------------------------------------------
/example/example-slack-message/src/SlackBubble.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react'
2 | import {
3 | Text,
4 | StyleSheet,
5 | TouchableOpacity,
6 | View,
7 | Platform,
8 | StyleProp,
9 | ViewStyle,
10 | TextStyle,
11 | } from 'react-native'
12 | import {
13 | MessageText,
14 | MessageImage,
15 | Time,
16 | utils,
17 | useChatContext,
18 | } from 'react-native-gifted-chat'
19 | import * as Clipboard from 'expo-clipboard'
20 |
21 | const { isSameUser, isSameDay } = utils
22 |
23 | interface Props {
24 | touchableProps: object
25 | onLongPress?: (context: unknown, currentMessage: object) => void
26 | renderMessageImage?: (props: Props) => React.ReactNode
27 | renderMessageText?: (props: Props) => React.ReactNode
28 | renderCustomView?: (props: Props) => React.ReactNode
29 | renderUsername?: (props: Props) => React.ReactNode
30 | renderTime?: (props: Props) => React.ReactNode
31 | renderTicks?: (currentMessage: object) => React.ReactNode
32 | currentMessage: object
33 | nextMessage: object
34 | previousMessage: object
35 | user: object
36 | containerStyle: {
37 | left: StyleProp
38 | right: StyleProp
39 | }
40 | wrapperStyle: {
41 | left: StyleProp
42 | right: StyleProp
43 | }
44 | messageTextStyle: StyleProp
45 | usernameStyle: StyleProp
46 | tickStyle: StyleProp
47 | containerToNextStyle: {
48 | left: StyleProp
49 | right: StyleProp
50 | }
51 | containerToPreviousStyle: {
52 | left: StyleProp
53 | right: StyleProp
54 | }
55 | imageStyle?: StyleProp
56 | textStyle: StyleProp
57 | position: 'left' | 'right'
58 | }
59 |
60 | const Bubble = (props: Props) => {
61 | const {
62 | touchableProps,
63 | onLongPress,
64 | renderCustomView,
65 | currentMessage,
66 | previousMessage,
67 | user,
68 | containerStyle,
69 | wrapperStyle,
70 | usernameStyle,
71 | tickStyle,
72 | position,
73 | } = props
74 |
75 | const context = useChatContext()
76 |
77 | const handleLongPress = useCallback(() => {
78 | if (onLongPress) {
79 | onLongPress(context, currentMessage)
80 | return
81 | }
82 |
83 | if (!currentMessage.text)
84 | return
85 |
86 | const options = ['Copy Text', 'Cancel']
87 | const cancelButtonIndex = options.length - 1
88 | context.actionSheet().showActionSheetWithOptions(
89 | {
90 | options,
91 | cancelButtonIndex,
92 | },
93 | (buttonIndex: number) => {
94 | switch (buttonIndex) {
95 | case 0:
96 | Clipboard.setStringAsync(currentMessage.text)
97 | break
98 | }
99 | }
100 | )
101 | }, [])
102 |
103 | const renderMessageText = useCallback(() => {
104 | if (currentMessage.text) {
105 | if (props.renderMessageText)
106 | return props.renderMessageText(props)
107 |
108 | return (
109 |
120 | )
121 | }
122 |
123 | return null
124 | }, [])
125 |
126 | const renderMessageImage = useCallback(() => {
127 | if (currentMessage.image) {
128 | if (props.renderMessageImage)
129 | return props.renderMessageImage(props)
130 |
131 | return (
132 |
136 | )
137 | }
138 |
139 | return null
140 | }, [])
141 |
142 | const renderTicks = useCallback(() => {
143 | const { currentMessage } = props
144 |
145 | if (props.renderTicks)
146 | return props.renderTicks(currentMessage)
147 |
148 | if (currentMessage.user._id !== user._id)
149 | return null
150 |
151 | if (currentMessage.sent || currentMessage.received)
152 | return (
153 |
154 | {currentMessage.sent && (
155 |
158 | ✓
159 |
160 | )}
161 | {currentMessage.received && (
162 |
165 | ✓
166 |
167 | )}
168 |
169 | )
170 |
171 | return null
172 | }, [])
173 |
174 | const renderUsername = useCallback(() => {
175 | const username = currentMessage.user.name
176 | if (username) {
177 | if (props.renderUsername)
178 | return props.renderUsername(props)
179 |
180 | return (
181 |
189 | {username}
190 |
191 | )
192 | }
193 | return null
194 | }, [])
195 |
196 | const renderTime = useCallback(() => {
197 | if (currentMessage.createdAt) {
198 | if (props.renderTime)
199 | return props.renderTime(props)
200 |
201 | return (
202 |
214 | )
215 | }
216 |
217 | return null
218 | }, [])
219 |
220 | const isSameThread = useMemo(() =>
221 | isSameUser(currentMessage, previousMessage) &&
222 | isSameDay(currentMessage, previousMessage)
223 | , [currentMessage, previousMessage])
224 |
225 | const messageHeader = useMemo(() => {
226 | if (isSameThread)
227 | return null
228 |
229 | return (
230 |
231 | {renderUsername()}
232 | {renderTime()}
233 | {renderTicks()}
234 |
235 | )
236 | }, [isSameThread, renderUsername, renderTime, renderTicks])
237 |
238 | return (
239 |
240 |
245 |
246 |
247 | {renderCustomView?.(props)}
248 | {messageHeader}
249 | {renderMessageImage()}
250 | {renderMessageText()}
251 |
252 |
253 |
254 |
255 | )
256 | }
257 |
258 | // Note: Everything is forced to be "left" positioned with this component.
259 | // The "right" position is only used in the default Bubble.
260 | const styles = StyleSheet.create({
261 | standardFont: {
262 | fontSize: 15,
263 | },
264 | slackMessageText: {
265 | marginLeft: 0,
266 | marginRight: 0,
267 | },
268 | container: {
269 | flex: 1,
270 | alignItems: 'flex-start',
271 | },
272 | wrapper: {
273 | marginRight: 60,
274 | minHeight: 20,
275 | justifyContent: 'flex-end',
276 | },
277 | username: {
278 | fontWeight: 'bold',
279 | },
280 | time: {
281 | textAlign: 'left',
282 | fontSize: 12,
283 | },
284 | timeContainer: {
285 | marginLeft: 0,
286 | marginRight: 0,
287 | marginBottom: 0,
288 | },
289 | headerItem: {
290 | marginRight: 10,
291 | },
292 | headerView: {
293 | // Try to align it better with the avatar on Android.
294 | marginTop: Platform.OS === 'android' ? -2 : 0,
295 | flexDirection: 'row',
296 | alignItems: 'baseline',
297 | },
298 | tick: {
299 | backgroundColor: 'transparent',
300 | color: 'white',
301 | },
302 | tickView: {
303 | flexDirection: 'row',
304 | },
305 | slackImage: {
306 | borderRadius: 3,
307 | marginLeft: 0,
308 | marginRight: 0,
309 | },
310 | })
311 |
312 | export default Bubble
313 |
--------------------------------------------------------------------------------
/example/example-slack-message/src/SlackMessage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react'
2 | import {
3 | View,
4 | StyleSheet,
5 | StyleProp,
6 | ViewStyle,
7 | } from 'react-native'
8 |
9 | import { Avatar, Day, utils } from 'react-native-gifted-chat'
10 | import type { DayProps, BubbleProps, AvatarProps, IMessage } from 'react-native-gifted-chat'
11 | import Bubble from './SlackBubble'
12 |
13 | const { isSameUser, isSameDay } = utils
14 |
15 | interface Props {
16 | renderAvatar?: (props: AvatarProps) => void,
17 | renderBubble?: (props: BubbleProps) => void,
18 | renderDay?: (props: DayProps) => void,
19 | currentMessage: any,
20 | nextMessage?: any,
21 | previousMessage?: any,
22 | containerStyle?: {
23 | left: StyleProp,
24 | right: StyleProp,
25 | },
26 | }
27 |
28 | const Message = (props: Props) => {
29 | const {
30 | currentMessage,
31 | nextMessage,
32 | previousMessage,
33 | containerStyle,
34 | } = props
35 |
36 | const getInnerComponentProps = useCallback(() => {
37 | return {
38 | ...props,
39 | position: 'left',
40 | isSameUser,
41 | isSameDay,
42 | containerStyle: props.containerStyle?.left,
43 | }
44 | }, [props])
45 |
46 | const renderDay = useCallback(() => {
47 | if (currentMessage.createdAt) {
48 | const dayProps = getInnerComponentProps()
49 |
50 | if (props.renderDay)
51 | return props.renderDay(dayProps)
52 |
53 | return
54 | }
55 |
56 | return null
57 | }, [])
58 |
59 | const renderBubble = useCallback(() => {
60 | const bubbleProps = getInnerComponentProps()
61 |
62 | if (props.renderBubble)
63 | return props.renderBubble(bubbleProps)
64 |
65 | return
66 | }, [])
67 |
68 | const renderAvatar = useCallback(() => {
69 | let extraStyle
70 | if (
71 | isSameUser(currentMessage, previousMessage) &&
72 | isSameDay(currentMessage, previousMessage)
73 | )
74 | // Set the invisible avatar height to 0, but keep the width, padding, etc.
75 | extraStyle = { height: 0 }
76 |
77 | const avatarProps = getInnerComponentProps()
78 |
79 | if (props.renderAvatar)
80 | return props.renderAvatar(avatarProps)
81 |
82 | return (
83 |
89 | )
90 | }, [])
91 |
92 | const marginBottom = useMemo(() =>
93 | isSameUser(
94 | currentMessage,
95 | nextMessage
96 | )
97 | ? 2
98 | : 10
99 | , [currentMessage, nextMessage])
100 |
101 | return (
102 |
103 | {renderDay()}
104 |
111 | {renderAvatar()}
112 | {renderBubble()}
113 |
114 |
115 | )
116 | }
117 |
118 | export default Message
119 |
120 | const styles = StyleSheet.create({
121 | container: {
122 | flexDirection: 'row',
123 | alignItems: 'flex-end',
124 | justifyContent: 'flex-start',
125 | marginLeft: 8,
126 | marginRight: 0,
127 | },
128 | slackAvatar: {
129 | // The bottom should roughly line up with the first line of message text.
130 | height: 40,
131 | width: 40,
132 | borderRadius: 3,
133 | },
134 | })
135 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo'
2 |
3 | import App from './App'
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App)
6 | // It also ensures that whether you load the app in Expo Go or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App)
9 |
--------------------------------------------------------------------------------
/example/ios/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 | .xcode.env.local
25 |
26 | # Bundle artifacts
27 | *.jsbundle
28 |
29 | # CocoaPods
30 | /Pods/
31 |
--------------------------------------------------------------------------------
/example/ios/.xcode.env:
--------------------------------------------------------------------------------
1 | # This `.xcode.env` file is versioned and is used to source the environment
2 | # used when running script phases inside Xcode.
3 | # To customize your local environment, you can create an `.xcode.env.local`
4 | # file that is not versioned.
5 |
6 | # NODE_BINARY variable contains the PATH to the node executable.
7 | #
8 | # Customize the NODE_BINARY variable here.
9 | # For example, to use nvm with brew, add the following line
10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use
11 | export NODE_BINARY=$(command -v node)
12 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
3 |
4 | require 'json'
5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
6 |
7 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
9 |
10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
11 | install! 'cocoapods',
12 | :deterministic_uuids => false
13 |
14 | prepare_react_native_project!
15 |
16 | target 'example' do
17 | use_expo_modules!
18 |
19 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
20 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
21 | else
22 | config_command = [
23 | 'node',
24 | '--no-warnings',
25 | '--eval',
26 | 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
27 | 'react-native-config',
28 | '--json',
29 | '--platform',
30 | 'ios'
31 | ]
32 | end
33 |
34 | config = use_native_modules!(config_command)
35 |
36 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
37 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
38 |
39 | use_react_native!(
40 | :path => config[:reactNativePath],
41 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
42 | # An absolute path to your application root.
43 | :app_path => "#{Pod::Config.instance.installation_root}/..",
44 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
45 | )
46 |
47 | post_install do |installer|
48 | react_native_post_install(
49 | installer,
50 | config[:reactNativePath],
51 | :mac_catalyst_enabled => false,
52 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
53 | )
54 |
55 | # This is necessary for Xcode 14, because it signs resource bundles by default
56 | # when building for devices.
57 | installer.target_installation_results.pod_target_installation_results
58 | .each do |pod_name, target_installation_result|
59 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
60 | resource_bundle_target.build_configurations.each do |config|
61 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/example/ios/Podfile.properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo.jsEngine": "hermes",
3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
4 | "apple.ccacheEnabled": "true"
5 | }
6 |
--------------------------------------------------------------------------------
/example/ios/example.xcodeproj/xcshareddata/xcschemes/example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/example/ios/example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/example/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import
4 |
5 | @interface AppDelegate : EXAppDelegateWrapper
6 |
7 | @end
8 |
--------------------------------------------------------------------------------
/example/ios/example/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 | #import
5 |
6 | @implementation AppDelegate
7 |
8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
9 | {
10 | self.moduleName = @"main";
11 |
12 | // You can add your custom initial props in the dictionary below.
13 | // They will be passed down to the ViewController used by React Native.
14 | self.initialProps = @{};
15 |
16 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
17 | }
18 |
19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
20 | {
21 | return [self bundleURL];
22 | }
23 |
24 | - (NSURL *)bundleURL
25 | {
26 | #if DEBUG
27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
28 | #else
29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
30 | #endif
31 | }
32 |
33 | // Linking API
34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options {
35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
36 | }
37 |
38 | // Universal Links
39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler {
40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
42 | }
43 |
44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
46 | {
47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
48 | }
49 |
50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
52 | {
53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
54 | }
55 |
56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
58 | {
59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
60 | }
61 |
62 | @end
63 |
--------------------------------------------------------------------------------
/example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "version" : 1,
11 | "author" : "expo"
12 | }
13 | }
--------------------------------------------------------------------------------
/example/ios/example/Images.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "expo"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/ios/example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSRequiresIPhoneOS
26 |
27 | LSMinimumSystemVersion
28 | 12.0
29 | NSAppTransportSecurity
30 |
31 | NSAllowsArbitraryLoads
32 |
33 | NSAllowsLocalNetworking
34 |
35 |
36 | UILaunchStoryboardName
37 | SplashScreen
38 | UIRequiredDeviceCapabilities
39 |
40 | arm64
41 |
42 | UIStatusBarStyle
43 | UIStatusBarStyleDefault
44 | UISupportedInterfaceOrientations
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationLandscapeLeft
48 | UIInterfaceOrientationLandscapeRight
49 |
50 | UIViewControllerBasedStatusBarAppearance
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/example/ios/example/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryUserDefaults
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | CA92.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryFileTimestamp
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | 0A2A.1
21 | 3B52.1
22 | C617.1
23 |
24 |
25 |
26 | NSPrivacyAccessedAPIType
27 | NSPrivacyAccessedAPICategoryDiskSpace
28 | NSPrivacyAccessedAPITypeReasons
29 |
30 | E174.1
31 | 85F4.1
32 |
33 |
34 |
35 | NSPrivacyAccessedAPIType
36 | NSPrivacyAccessedAPICategorySystemBootTime
37 | NSPrivacyAccessedAPITypeReasons
38 |
39 | 35F9.1
40 |
41 |
42 |
43 | NSPrivacyCollectedDataTypes
44 |
45 | NSPrivacyTracking
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/example/ios/example/SplashScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/example/ios/example/Supporting/Expo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/example/ios/example/main.m:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char * argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Metro configuration
3 | * https://facebook.github.io/metro/docs/configuration
4 | *
5 | * @type {import('metro-config').MetroConfig}
6 | */
7 |
8 | /* eslint-disable @typescript-eslint/no-require-imports */
9 | const { mergeConfig } = require('@react-native/metro-config')
10 | const { getDefaultConfig } = require('@expo/metro-config')
11 | const path = require('path')
12 | // const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config')
13 |
14 | /* eslint-enable @typescript-eslint/no-require-imports */
15 | const config = {
16 | watchFolders: [
17 | path.resolve(__dirname, '../src'),
18 | ],
19 | resolver: {
20 | extraNodeModules: new Proxy(
21 | {},
22 | {
23 | get: (target, name) => {
24 | // console.log(`example/metro name: ${name}`, Object.prototype.hasOwnProperty.call(target, name))
25 | if (Object.prototype.hasOwnProperty.call(target, name))
26 | return target[name]
27 |
28 | if (name === 'react-native-gifted-chat')
29 | return path.join(process.cwd(), '../src')
30 |
31 | return path.join(process.cwd(), `node_modules/${name}`)
32 | },
33 | }
34 | ),
35 | },
36 | }
37 |
38 | // module.exports = wrapWithReanimatedMetroConfig(mergeConfig(getDefaultConfig(__dirname), config))
39 | module.exports = mergeConfig(getDefaultConfig(__dirname), config)
40 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "android": "expo run:android",
7 | "eject": "expo eject",
8 | "ios": "expo run:ios",
9 | "start": "expo start --dev-client",
10 | "test": "jest",
11 | "web": "expo start --web",
12 | "fresh": "yarn start --reset-cache"
13 | },
14 | "dependencies": {
15 | "expo": "^52.0.40",
16 | "expo-app-loading": "^2.1.1",
17 | "expo-build-properties": "~0.13.2",
18 | "expo-clipboard": "^7.0.1",
19 | "expo-image-picker": "^16.0.6",
20 | "expo-linking": "^7.0.5",
21 | "expo-location": "^18.0.8",
22 | "expo-permissions": "^14.4.0",
23 | "expo-status-bar": "^2.0.1",
24 | "react": "18.3.1",
25 | "react-dom": "18.3.1",
26 | "react-native": "0.76.7",
27 | "react-native-gifted-chat": "^2.8.0",
28 | "react-native-keyboard-controller": "^1.16.8",
29 | "react-native-maps": "^1.20.1",
30 | "react-native-paper": "^5.13.1",
31 | "react-native-reanimated": "^3.17.1",
32 | "react-native-safe-area-context": "^5.3.0"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.26.10",
36 | "@react-native/metro-config": "0.76.6",
37 | "@react-native/typescript-config": "0.76.6",
38 | "@types/react": "^18.3.3",
39 | "@types/react-test-renderer": "^18.3.0",
40 | "babel-plugin-module-resolver": "^5.0.2",
41 | "jest": "^29.7.0",
42 | "react-test-renderer": "18.3.1",
43 | "typescript": "5.8.2"
44 | },
45 | "engines": {
46 | "node": ">=18"
47 | },
48 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
49 | }
50 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "react-native-gifted-chat": ["../src/index"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | resetMocks: true,
4 | setupFiles: [
5 | './node_modules/react-native/jest-preset',
6 | // './node_modules/react-native/jest/setup.js',
7 | './tests/setup.js',
8 | ],
9 | moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],
10 | transform: {
11 | '\\.js$': ['babel-jest', { configFile: './babel.config.cjs' }],
12 | },
13 | transformIgnorePatterns: [],
14 | testMatch: ['**/*.test.ts?(x)'],
15 | modulePathIgnorePatterns: ['./example'],
16 | coveragePathIgnorePatterns: ['./src/__tests__/'],
17 | }
18 |
--------------------------------------------------------------------------------
/media/logo_sponsor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/media/logo_sponsor.png
--------------------------------------------------------------------------------
/media/stream-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/media/stream-logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-gifted-chat",
3 | "version": "2.8.1",
4 | "description": "The most complete chat UI for React Native",
5 | "keywords": [
6 | "android",
7 | "ios",
8 | "react-native",
9 | "react",
10 | "react-component",
11 | "messenger",
12 | "message",
13 | "chat"
14 | ],
15 | "homepage": "https://github.com/FaridSafi/react-native-gifted-chat#readme",
16 | "bugs": {
17 | "url": "https://github.com/FaridSafi/react-native-gifted-chat/issues"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/FaridSafi/react-native-gifted-chat.git"
22 | },
23 | "license": "MIT",
24 | "author": "Farid Safi",
25 | "type": "module",
26 | "main": "lib/index.js",
27 | "types": "lib/index.d.ts",
28 | "files": [
29 | "src",
30 | "lib"
31 | ],
32 | "scripts": {
33 | "build": "rm -rf lib/ && yarn tsc",
34 | "lint": "yarn eslint src",
35 | "lint:fix": "yarn eslint --cache --fix",
36 | "prepublishOnly": "yarn lint && yarn build && yarn test",
37 | "start": "cd example && expo start",
38 | "start:web": "cd example && expo start -w --dev",
39 | "test": "TZ=Europe/Paris jest --no-watchman",
40 | "test:coverage": "TZ=Europe/Paris jest --coverage",
41 | "test:watch": "TZ=Europe/Paris jest --watch",
42 | "tsc:write": "yarn tsc --project tsconfig.json",
43 | "tsc:watch": "yarn tsc --watch --noEmit",
44 | "fresh": "yarn start --reset-cache",
45 | "prepare": "yarn husky"
46 | },
47 | "lint-staged": {
48 | "src/*.{json,js,jsx,ts,tsx}": [
49 | "yarn lint:fix",
50 | "bash -c 'yarn tsc:write'"
51 | ]
52 | },
53 | "dependencies": {
54 | "@expo/react-native-action-sheet": "^4.1.1",
55 | "@types/lodash.isequal": "^4.5.8",
56 | "dayjs": "^1.11.13",
57 | "lodash.isequal": "^4.5.0",
58 | "react-native-communications": "^2.2.1",
59 | "react-native-iphone-x-helper": "^1.3.1",
60 | "react-native-lightbox-v2": "^0.9.2",
61 | "react-native-parsed-text": "^0.0.22"
62 | },
63 | "devDependencies": {
64 | "@babel/core": "^7.26.10",
65 | "@babel/plugin-transform-react-jsx": "^7.25.9",
66 | "@babel/plugin-transform-unicode-property-regex": "^7.25.9",
67 | "@babel/preset-env": "^7.26.9",
68 | "@react-native/eslint-config": "^0.76.6",
69 | "@stylistic/eslint-plugin": "^3.1.0",
70 | "@types/jest": "^29.5.14",
71 | "@types/react": "^19.0.10",
72 | "@types/react-dom": "^19.0.4",
73 | "@types/react-native": "^0.72.8",
74 | "@typescript-eslint/eslint-plugin": "^8.28.0",
75 | "@typescript-eslint/parser": "^8.28.0",
76 | "babel-jest": "^29.7.0",
77 | "eslint": "^8.57.0",
78 | "eslint-config-standard": "^17.1.0",
79 | "eslint-config-standard-jsx": "^11.0.0",
80 | "eslint-plugin-import": "^2.31.0",
81 | "eslint-plugin-jest": "^28.11.0",
82 | "eslint-plugin-json": "^4.0.1",
83 | "eslint-plugin-n": "^17.17.0",
84 | "eslint-plugin-node": "^11.1.0",
85 | "eslint-plugin-promise": "^7.2.1",
86 | "eslint-plugin-react": "^7.37.4",
87 | "husky": "^9.1.7",
88 | "jest": "^29.7.0",
89 | "json": "^11.0.0",
90 | "lint-staged": "^15.5.0",
91 | "react": "^18.3.1",
92 | "react-dom": "^18.3.1",
93 | "react-native": "^0.76.6",
94 | "react-native-keyboard-controller": "^1.16.8",
95 | "react-native-reanimated": "^3.17.1",
96 | "react-test-renderer": "^18.3.1",
97 | "typescript": "^5.8.2"
98 | },
99 | "peerDependencies": {
100 | "react": ">=18.0.0",
101 | "react-native": "*",
102 | "react-native-keyboard-controller": ">=1.0.0",
103 | "react-native-reanimated": ">=3.0.0"
104 | },
105 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
106 | "engines": {
107 | "node": ">=18"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/screenshots/gifted-chat-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/screenshots/gifted-chat-1.png
--------------------------------------------------------------------------------
/screenshots/gifted-chat-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/screenshots/gifted-chat-2.png
--------------------------------------------------------------------------------
/screenshots/iPhone-6s-gifted-chat-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/screenshots/iPhone-6s-gifted-chat-1.png
--------------------------------------------------------------------------------
/screenshots/iPhone-6s-gifted-chat-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/screenshots/iPhone-6s-gifted-chat-2.png
--------------------------------------------------------------------------------
/screenshots/iPhone-6s-gifted-chat-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/7766a68fc753bac74b7b7d715772783b47fb8b46/screenshots/iPhone-6s-gifted-chat-3.png
--------------------------------------------------------------------------------
/src/Actions.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from 'react'
2 | import {
3 | StyleSheet,
4 | Text,
5 | TouchableOpacity,
6 | View,
7 | StyleProp,
8 | ViewStyle,
9 | TextStyle,
10 | } from 'react-native'
11 | import Color from './Color'
12 | import { useChatContext } from './GiftedChatContext'
13 |
14 | import stylesCommon from './styles'
15 |
16 | export interface ActionsProps {
17 | options?: { [key: string]: () => void }
18 | optionTintColor?: string
19 | icon?: () => ReactNode
20 | wrapperStyle?: StyleProp
21 | iconTextStyle?: StyleProp
22 | containerStyle?: StyleProp
23 | onPressActionButton?(): void
24 | }
25 |
26 | export function Actions ({
27 | options,
28 | optionTintColor = Color.optionTintColor,
29 | icon,
30 | wrapperStyle,
31 | iconTextStyle,
32 | onPressActionButton,
33 | containerStyle,
34 | }: ActionsProps) {
35 | const { actionSheet } = useChatContext()
36 |
37 | const onActionsPress = useCallback(() => {
38 | if (!options)
39 | return
40 |
41 | const optionKeys = Object.keys(options)
42 | const cancelButtonIndex = optionKeys.indexOf('Cancel')
43 |
44 | actionSheet().showActionSheetWithOptions(
45 | {
46 | options: optionKeys,
47 | cancelButtonIndex,
48 | tintColor: optionTintColor,
49 | },
50 | (buttonIndex: number | undefined) => {
51 | if (buttonIndex === undefined)
52 | return
53 |
54 | const key = optionKeys[buttonIndex]
55 | if (key)
56 | options[key]()
57 | }
58 | )
59 | }, [actionSheet, options, optionTintColor])
60 |
61 | const renderIcon = useCallback(() => {
62 | if (icon)
63 | return icon()
64 |
65 | return (
66 |
67 | {'+'}
68 |
69 | )
70 | }, [icon, iconTextStyle, wrapperStyle])
71 |
72 | return (
73 |
77 | {renderIcon()}
78 |
79 | )
80 | }
81 |
82 | const styles = StyleSheet.create({
83 | container: {
84 | width: 26,
85 | height: 26,
86 | marginLeft: 10,
87 | marginBottom: 10,
88 | },
89 | wrapper: {
90 | borderRadius: 13,
91 | borderColor: Color.defaultColor,
92 | borderWidth: 2,
93 | },
94 | iconText: {
95 | color: Color.defaultColor,
96 | fontWeight: 'bold',
97 | fontSize: 16,
98 | lineHeight: 16,
99 | backgroundColor: Color.backgroundTransparent,
100 | textAlign: 'center',
101 | },
102 | })
103 |
--------------------------------------------------------------------------------
/src/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from 'react'
2 | import {
3 | ImageStyle,
4 | StyleSheet,
5 | TextStyle,
6 | View,
7 | ViewStyle,
8 | } from 'react-native'
9 | import { GiftedAvatar } from './GiftedAvatar'
10 | import { isSameUser, isSameDay } from './utils'
11 | import { IMessage, LeftRightStyle, User } from './types'
12 |
13 | interface Styles {
14 | left: {
15 | container: ViewStyle
16 | onTop: ViewStyle
17 | image: ImageStyle
18 | }
19 | right: {
20 | container: ViewStyle
21 | onTop: ViewStyle
22 | image: ImageStyle
23 | }
24 | }
25 |
26 | const styles: Styles = {
27 | left: StyleSheet.create({
28 | container: {
29 | marginRight: 8,
30 | },
31 | onTop: {
32 | alignSelf: 'flex-start',
33 | },
34 | image: {
35 | height: 36,
36 | width: 36,
37 | borderRadius: 18,
38 | },
39 | }),
40 | right: StyleSheet.create({
41 | container: {
42 | marginLeft: 8,
43 | },
44 | onTop: {
45 | alignSelf: 'flex-start',
46 | },
47 | image: {
48 | height: 36,
49 | width: 36,
50 | borderRadius: 18,
51 | },
52 | }),
53 | }
54 |
55 | export interface AvatarProps {
56 | currentMessage: TMessage
57 | previousMessage?: TMessage
58 | nextMessage?: TMessage
59 | position: 'left' | 'right'
60 | renderAvatarOnTop?: boolean
61 | showAvatarForEveryMessage?: boolean
62 | imageStyle?: LeftRightStyle
63 | containerStyle?: LeftRightStyle
64 | textStyle?: TextStyle
65 | renderAvatar?(props: Omit, 'renderAvatar'>): ReactNode
66 | onPressAvatar?: (user: User) => void
67 | onLongPressAvatar?: (user: User) => void
68 | }
69 |
70 | export function Avatar (
71 | props: AvatarProps
72 | ) {
73 | const {
74 | renderAvatarOnTop,
75 | showAvatarForEveryMessage,
76 | containerStyle,
77 | position,
78 | currentMessage,
79 | renderAvatar,
80 | previousMessage,
81 | nextMessage,
82 | imageStyle,
83 | onPressAvatar,
84 | onLongPressAvatar,
85 | } = props
86 |
87 | const messageToCompare = renderAvatarOnTop ? previousMessage : nextMessage
88 |
89 | const renderAvatarComponent = useCallback(() => {
90 | if (renderAvatar)
91 | return renderAvatar({
92 | renderAvatarOnTop,
93 | showAvatarForEveryMessage,
94 | containerStyle,
95 | position,
96 | currentMessage,
97 | previousMessage,
98 | nextMessage,
99 | imageStyle,
100 | onPressAvatar,
101 | onLongPressAvatar,
102 | })
103 |
104 | if (currentMessage)
105 | return (
106 | onPressAvatar?.(currentMessage.user)}
113 | onLongPress={() => onLongPressAvatar?.(currentMessage.user)}
114 | />
115 | )
116 |
117 | return null
118 | }, [
119 | renderAvatar,
120 | renderAvatarOnTop,
121 | showAvatarForEveryMessage,
122 | containerStyle,
123 | position,
124 | currentMessage,
125 | previousMessage,
126 | nextMessage,
127 | imageStyle,
128 | onPressAvatar,
129 | onLongPressAvatar,
130 | ])
131 |
132 | if (renderAvatar === null)
133 | return null
134 |
135 | if (
136 | !showAvatarForEveryMessage &&
137 | currentMessage &&
138 | messageToCompare &&
139 | isSameUser(currentMessage, messageToCompare) &&
140 | isSameDay(currentMessage, messageToCompare)
141 | )
142 | return (
143 |
149 |
155 |
156 | )
157 |
158 | return (
159 |
166 | {renderAvatarComponent()}
167 |
168 | )
169 | }
170 |
--------------------------------------------------------------------------------
/src/Bubble/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 | import Color from '../Color'
3 |
4 | const styles = {
5 | left: StyleSheet.create({
6 | container: {
7 | alignItems: 'flex-start',
8 | },
9 | wrapper: {
10 | borderRadius: 15,
11 | backgroundColor: Color.leftBubbleBackground,
12 | marginRight: 60,
13 | minHeight: 20,
14 | justifyContent: 'flex-end',
15 | },
16 | containerToNext: {
17 | borderBottomLeftRadius: 3,
18 | },
19 | containerToPrevious: {
20 | borderTopLeftRadius: 3,
21 | },
22 | bottom: {
23 | flexDirection: 'row',
24 | justifyContent: 'flex-start',
25 | },
26 | }),
27 | right: StyleSheet.create({
28 | container: {
29 | alignItems: 'flex-end',
30 | },
31 | wrapper: {
32 | borderRadius: 15,
33 | backgroundColor: Color.defaultBlue,
34 | marginLeft: 60,
35 | minHeight: 20,
36 | justifyContent: 'flex-end',
37 | },
38 | containerToNext: {
39 | borderBottomRightRadius: 3,
40 | },
41 | containerToPrevious: {
42 | borderTopRightRadius: 3,
43 | },
44 | bottom: {
45 | flexDirection: 'row',
46 | justifyContent: 'flex-end',
47 | },
48 | }),
49 | content: StyleSheet.create({
50 | tick: {
51 | fontSize: 10,
52 | backgroundColor: Color.backgroundTransparent,
53 | color: Color.white,
54 | },
55 | tickView: {
56 | flexDirection: 'row',
57 | marginRight: 10,
58 | },
59 | username: {
60 | top: -3,
61 | left: 0,
62 | fontSize: 12,
63 | backgroundColor: Color.backgroundTransparent,
64 | color: '#aaa',
65 | },
66 | usernameView: {
67 | flexDirection: 'row',
68 | marginHorizontal: 10,
69 | },
70 | }),
71 | }
72 |
73 | export default styles
74 |
--------------------------------------------------------------------------------
/src/Bubble/types.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | StyleProp,
4 | ViewStyle,
5 | TextStyle,
6 | } from 'react-native'
7 | import { QuickRepliesProps } from '../QuickReplies'
8 | import { MessageTextProps } from '../MessageText'
9 | import { MessageImageProps } from '../MessageImage'
10 | import { TimeProps } from '../Time'
11 | import {
12 | User,
13 | IMessage,
14 | LeftRightStyle,
15 | Reply,
16 | Omit,
17 | MessageVideoProps,
18 | MessageAudioProps,
19 | } from '../types'
20 |
21 | /* eslint-disable no-use-before-define */
22 | export type RenderMessageImageProps = Omit<
23 | BubbleProps,
24 | 'containerStyle' | 'wrapperStyle'
25 | > &
26 | MessageImageProps
27 |
28 | export type RenderMessageVideoProps = Omit<
29 | BubbleProps,
30 | 'containerStyle' | 'wrapperStyle'
31 | > &
32 | MessageVideoProps
33 |
34 | export type RenderMessageAudioProps = Omit<
35 | BubbleProps,
36 | 'containerStyle' | 'wrapperStyle'
37 | > &
38 | MessageAudioProps
39 |
40 | export type RenderMessageTextProps = Omit<
41 | BubbleProps,
42 | 'containerStyle' | 'wrapperStyle'
43 | > &
44 | MessageTextProps
45 | /* eslint-enable no-use-before-define */
46 |
47 | export interface BubbleProps {
48 | user?: User
49 | touchableProps?: object
50 | renderUsernameOnMessage?: boolean
51 | isCustomViewBottom?: boolean
52 | inverted?: boolean
53 | position: 'left' | 'right'
54 | currentMessage: TMessage
55 | nextMessage?: TMessage
56 | previousMessage?: TMessage
57 | optionTitles?: string[]
58 | containerStyle?: LeftRightStyle
59 | wrapperStyle?: LeftRightStyle
60 | textStyle?: LeftRightStyle
61 | bottomContainerStyle?: LeftRightStyle
62 | tickStyle?: StyleProp
63 | containerToNextStyle?: LeftRightStyle
64 | containerToPreviousStyle?: LeftRightStyle
65 | usernameStyle?: TextStyle
66 | quickReplyStyle?: StyleProp
67 | quickReplyTextStyle?: StyleProp
68 | quickReplyContainerStyle?: StyleProp
69 | onPress?(context?: unknown, message?: unknown): void
70 | onLongPress?(context?: unknown, message?: unknown): void
71 | onQuickReply?(replies: Reply[]): void
72 | renderMessageImage?(
73 | props: RenderMessageImageProps,
74 | ): React.ReactNode
75 | renderMessageVideo?(
76 | props: RenderMessageVideoProps,
77 | ): React.ReactNode
78 | renderMessageAudio?(
79 | props: RenderMessageAudioProps,
80 | ): React.ReactNode
81 | renderMessageText?(props: RenderMessageTextProps): React.ReactNode
82 | renderCustomView?(bubbleProps: BubbleProps): React.ReactNode
83 | renderTime?(timeProps: TimeProps): React.ReactNode
84 | renderTicks?(currentMessage: TMessage): React.ReactNode
85 | renderUsername?(user?: TMessage['user']): React.ReactNode
86 | renderQuickReplySend?(): React.ReactNode
87 | renderQuickReplies?(
88 | quickReplies: QuickRepliesProps,
89 | ): React.ReactNode
90 | }
91 |
--------------------------------------------------------------------------------
/src/Color.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | defaultColor: '#b2b2b2',
3 | backgroundTransparent: 'transparent',
4 | defaultBlue: '#0084ff',
5 | leftBubbleBackground: '#f0f0f0',
6 | black: '#000',
7 | white: '#fff',
8 | carrot: '#e67e22',
9 | emerald: '#2ecc71',
10 | peterRiver: '#3498db',
11 | wisteria: '#8e44ad',
12 | alizarin: '#e74c3c',
13 | turquoise: '#1abc9c',
14 | midnightBlue: '#2c3e50',
15 | optionTintColor: '#007AFF',
16 | timeTextColor: '#aaa',
17 | }
18 |
--------------------------------------------------------------------------------
/src/Composer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef } from 'react'
2 | import {
3 | Platform,
4 | StyleSheet,
5 | TextInput,
6 | TextInputProps,
7 | NativeSyntheticEvent,
8 | TextInputContentSizeChangeEventData,
9 | } from 'react-native'
10 | import { MIN_COMPOSER_HEIGHT, DEFAULT_PLACEHOLDER } from './Constant'
11 | import Color from './Color'
12 | import stylesCommon from './styles'
13 |
14 | export interface ComposerProps {
15 | composerHeight?: number
16 | text?: string
17 | placeholder?: string
18 | placeholderTextColor?: string
19 | textInputProps?: Partial
20 | textInputStyle?: TextInputProps['style']
21 | textInputAutoFocus?: boolean
22 | keyboardAppearance?: TextInputProps['keyboardAppearance']
23 | multiline?: boolean
24 | disableComposer?: boolean
25 | onTextChanged?(text: string): void
26 | onInputSizeChanged?(layout: { width: number, height: number }): void
27 | }
28 |
29 | export function Composer ({
30 | composerHeight = MIN_COMPOSER_HEIGHT,
31 | disableComposer = false,
32 | keyboardAppearance = 'default',
33 | multiline = true,
34 | onInputSizeChanged,
35 | onTextChanged,
36 | placeholder = DEFAULT_PLACEHOLDER,
37 | placeholderTextColor = Color.defaultColor,
38 | text = '',
39 | textInputAutoFocus = false,
40 | textInputProps,
41 | textInputStyle,
42 | }: ComposerProps): React.ReactElement {
43 | const dimensionsRef = useRef<{ width: number, height: number }>(null)
44 |
45 | const determineInputSizeChange = useCallback(
46 | (dimensions: { width: number, height: number }) => {
47 | // Support earlier versions of React Native on Android.
48 | if (!dimensions)
49 | return
50 |
51 | if (
52 | !dimensionsRef.current ||
53 | (dimensionsRef.current &&
54 | (dimensionsRef.current.width !== dimensions.width ||
55 | dimensionsRef.current.height !== dimensions.height))
56 | ) {
57 | dimensionsRef.current = dimensions
58 | onInputSizeChanged?.(dimensions)
59 | }
60 | },
61 | [onInputSizeChanged]
62 | )
63 |
64 | const handleContentSizeChange = useCallback(
65 | ({
66 | nativeEvent: { contentSize },
67 | }: NativeSyntheticEvent) =>
68 | determineInputSizeChange(contentSize),
69 | [determineInputSizeChange]
70 | )
71 |
72 | return (
73 |
105 | )
106 | }
107 |
108 | const styles = StyleSheet.create({
109 | textInput: {
110 | marginLeft: 10,
111 | fontSize: 16,
112 | lineHeight: 22,
113 | ...Platform.select({
114 | web: {
115 | paddingTop: 6,
116 | paddingLeft: 4,
117 | },
118 | }),
119 | marginTop: Platform.select({
120 | ios: 6,
121 | android: 0,
122 | web: 6,
123 | }),
124 | marginBottom: Platform.select({
125 | ios: 5,
126 | android: 3,
127 | web: 4,
128 | }),
129 | },
130 | })
131 |
--------------------------------------------------------------------------------
/src/Constant.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from 'react-native'
2 |
3 | export const MIN_COMPOSER_HEIGHT = Platform.select({
4 | ios: 33,
5 | android: 41,
6 | web: 34,
7 | windows: 34,
8 | })
9 | export const MAX_COMPOSER_HEIGHT = 200
10 | export const DEFAULT_PLACEHOLDER = 'Type a message...'
11 | export const DATE_FORMAT = 'D MMMM'
12 | export const TIME_FORMAT = 'LT'
13 |
14 | export const TEST_ID = {
15 | WRAPPER: 'GC_WRAPPER',
16 | LOADING_WRAPPER: 'GC_LOADING_CONTAINER',
17 | SEND_TOUCHABLE: 'GC_SEND_TOUCHABLE',
18 | }
19 |
--------------------------------------------------------------------------------
/src/Day/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import {
3 | Text,
4 | View,
5 | } from 'react-native'
6 | import dayjs from 'dayjs'
7 | import relativeTime from 'dayjs/plugin/relativeTime'
8 | import calendar from 'dayjs/plugin/calendar'
9 |
10 | import { DATE_FORMAT } from '../Constant'
11 | import { DayProps } from './types'
12 |
13 | import { useChatContext } from '../GiftedChatContext'
14 | import stylesCommon from '../styles'
15 | import styles from './styles'
16 |
17 | export * from './types'
18 |
19 | dayjs.extend(relativeTime)
20 | dayjs.extend(calendar)
21 |
22 | export function Day ({
23 | dateFormat = DATE_FORMAT,
24 | dateFormatCalendar,
25 | createdAt,
26 | containerStyle,
27 | wrapperStyle,
28 | textStyle,
29 | }: DayProps) {
30 | const { getLocale } = useChatContext()
31 |
32 | const dateStr = useMemo(() => {
33 | if (createdAt == null)
34 | return null
35 |
36 | const now = dayjs().startOf('day')
37 | const date = dayjs(createdAt).locale(getLocale()).startOf('day')
38 |
39 | if (!now.isSame(date, 'year'))
40 | return date.format('D MMMM YYYY')
41 |
42 | if (now.diff(date, 'days') < 1)
43 | return date.calendar(now, {
44 | sameDay: '[Today]',
45 | ...dateFormatCalendar,
46 | })
47 |
48 | return date.format(dateFormat)
49 | }, [createdAt, dateFormat, getLocale, dateFormatCalendar])
50 |
51 | if (!dateStr)
52 | return null
53 |
54 | return (
55 |
56 |
57 |
58 | {dateStr}
59 |
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/Day/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 | import Color from '../Color'
3 |
4 | export default StyleSheet.create({
5 | container: {
6 | marginTop: 5,
7 | marginBottom: 10,
8 | },
9 | wrapper: {
10 | backgroundColor: 'rgba(0, 0, 0, 0.75)',
11 | paddingTop: 6,
12 | paddingBottom: 6,
13 | paddingLeft: 10,
14 | paddingRight: 10,
15 | borderRadius: 15,
16 | },
17 | text: {
18 | color: Color.white,
19 | fontSize: 12,
20 | fontWeight: '600',
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/src/Day/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | StyleProp,
3 | ViewStyle,
4 | TextStyle,
5 | } from 'react-native'
6 |
7 | export interface DayProps {
8 | createdAt: Date | number
9 | dateFormat?: string
10 | dateFormatCalendar?: object
11 | containerStyle?: StyleProp
12 | wrapperStyle?: StyleProp
13 | textStyle?: StyleProp
14 | }
15 |
--------------------------------------------------------------------------------
/src/GiftedAvatar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react'
2 | import {
3 | Image,
4 | Text,
5 | TouchableOpacity,
6 | View,
7 | StyleSheet,
8 | StyleProp,
9 | ImageStyle,
10 | TextStyle,
11 | } from 'react-native'
12 | import Color from './Color'
13 | import { User } from './types'
14 | import stylesCommon from './styles'
15 |
16 | const {
17 | carrot,
18 | emerald,
19 | peterRiver,
20 | wisteria,
21 | alizarin,
22 | turquoise,
23 | midnightBlue,
24 | } = Color
25 |
26 | const styles = StyleSheet.create({
27 | avatarStyle: {
28 | width: 40,
29 | height: 40,
30 | borderRadius: 20,
31 | },
32 | avatarTransparent: {
33 | backgroundColor: Color.backgroundTransparent,
34 | },
35 | textStyle: {
36 | color: Color.white,
37 | fontSize: 16,
38 | backgroundColor: Color.backgroundTransparent,
39 | fontWeight: '100',
40 | },
41 | })
42 |
43 | export interface GiftedAvatarProps {
44 | user?: User
45 | avatarStyle?: StyleProp
46 | textStyle?: StyleProp
47 | onPress?: (props: GiftedAvatarProps) => void
48 | onLongPress?: (props: GiftedAvatarProps) => void
49 | }
50 |
51 | export function GiftedAvatar (
52 | props: GiftedAvatarProps
53 | ) {
54 | const [avatarName, setAvatarName] = useState(undefined)
55 | const [backgroundColor, setBackgroundColor] = useState(undefined)
56 |
57 | const {
58 | user,
59 | avatarStyle,
60 | textStyle,
61 | onPress,
62 | } = props
63 |
64 | const setAvatarColor = useCallback(() => {
65 | if (backgroundColor)
66 | return
67 |
68 | const userName = user?.name || ''
69 | const name = userName.toUpperCase().split(' ')
70 |
71 | if (name.length === 1)
72 | setAvatarName(`${name[0].charAt(0)}`)
73 | else if (name.length > 1)
74 | setAvatarName(`${name[0].charAt(0)}${name[1].charAt(0)}`)
75 | else
76 | setAvatarName('')
77 |
78 | let sumChars = 0
79 | for (let i = 0; i < userName.length; i += 1)
80 | sumChars += userName.charCodeAt(i)
81 |
82 | // inspired by https://github.com/wbinnssmith/react-user-avatar
83 | // colors from https://flatuicolors.com/
84 | const colors = [
85 | carrot,
86 | emerald,
87 | peterRiver,
88 | wisteria,
89 | alizarin,
90 | turquoise,
91 | midnightBlue,
92 | ]
93 |
94 | setBackgroundColor(colors[sumChars % colors.length])
95 | }, [user?.name, backgroundColor])
96 |
97 | const renderAvatar = useCallback(() => {
98 | switch (typeof user?.avatar) {
99 | case 'function':
100 | return user.avatar([stylesCommon.centerItems, styles.avatarStyle, avatarStyle])
101 | case 'string':
102 | return (
103 |
107 | )
108 | case 'number':
109 | return (
110 |
114 | )
115 | default:
116 | return null
117 | }
118 | }, [user, avatarStyle])
119 |
120 | const renderInitials = useCallback(() => {
121 | return (
122 |
123 | {avatarName}
124 |
125 | )
126 | }, [textStyle, avatarName])
127 |
128 | const handleOnPress = () => {
129 | const {
130 | onPress,
131 | ...rest
132 | } = props
133 |
134 | if (onPress)
135 | onPress(rest)
136 | }
137 |
138 | const handleOnLongPress = () => {
139 | const {
140 | onLongPress,
141 | ...rest
142 | } = props
143 |
144 | if (onLongPress)
145 | onLongPress(rest)
146 | }
147 |
148 | useEffect(() => {
149 | setAvatarColor()
150 | }, [setAvatarColor])
151 |
152 | if (!user || (!user.name && !user.avatar))
153 | // render placeholder
154 | return (
155 |
164 | )
165 |
166 | if (user.avatar)
167 | return (
168 |
174 | {renderAvatar()}
175 |
176 | )
177 |
178 | return (
179 |
191 | {renderInitials()}
192 |
193 | )
194 | }
195 |
--------------------------------------------------------------------------------
/src/GiftedChat/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | export default StyleSheet.create({
4 | contentContainer: {
5 | overflow: 'hidden',
6 | },
7 | })
8 |
--------------------------------------------------------------------------------
/src/GiftedChatContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react'
2 | import {
3 | ActionSheetOptions,
4 | } from '@expo/react-native-action-sheet'
5 |
6 | export interface IGiftedChatContext {
7 | actionSheet(): {
8 | showActionSheetWithOptions: (
9 | options: ActionSheetOptions,
10 | callback: (buttonIndex?: number) => void | Promise
11 | ) => void
12 | }
13 | getLocale(): string
14 | }
15 |
16 | export const GiftedChatContext = createContext({
17 | getLocale: () => 'en',
18 | actionSheet: () => ({
19 | showActionSheetWithOptions: () => {},
20 | }),
21 | })
22 |
23 | export const useChatContext = () => useContext(GiftedChatContext)
24 |
--------------------------------------------------------------------------------
/src/InputToolbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native'
3 |
4 | import { Composer, ComposerProps } from './Composer'
5 | import { Send, SendProps } from './Send'
6 | import { Actions, ActionsProps } from './Actions'
7 | import Color from './Color'
8 | import { IMessage } from './types'
9 |
10 | export interface InputToolbarProps {
11 | options?: { [key: string]: () => void }
12 | optionTintColor?: string
13 | containerStyle?: StyleProp
14 | primaryStyle?: StyleProp
15 | accessoryStyle?: StyleProp
16 | renderAccessory?(props: InputToolbarProps): React.ReactNode
17 | renderActions?(props: ActionsProps): React.ReactNode
18 | renderSend?(props: SendProps): React.ReactNode
19 | renderComposer?(props: ComposerProps): React.ReactNode
20 | onPressActionButton?(): void
21 | icon?: () => React.ReactNode
22 | wrapperStyle?: StyleProp
23 | }
24 |
25 | export function InputToolbar (
26 | props: InputToolbarProps
27 | ) {
28 | const {
29 | renderActions,
30 | onPressActionButton,
31 | renderComposer,
32 | renderSend,
33 | renderAccessory,
34 | options,
35 | optionTintColor,
36 | icon,
37 | wrapperStyle,
38 | containerStyle,
39 | } = props
40 |
41 | const actionsFragment = useMemo(() => {
42 | const props = {
43 | onPressActionButton,
44 | options,
45 | optionTintColor,
46 | icon,
47 | wrapperStyle,
48 | containerStyle,
49 | }
50 |
51 | return (
52 | renderActions?.(props) || (onPressActionButton && )
53 | )
54 | }, [
55 | renderActions,
56 | onPressActionButton,
57 | options,
58 | optionTintColor,
59 | icon,
60 | wrapperStyle,
61 | containerStyle,
62 | ])
63 |
64 | const composerFragment = useMemo(() => {
65 | return (
66 | renderComposer?.(props as ComposerProps) || (
67 |
68 | )
69 | )
70 | }, [renderComposer, props])
71 |
72 | return (
73 |
74 |
75 | {actionsFragment}
76 | {composerFragment}
77 | {renderSend?.(props) || }
78 |
79 | {renderAccessory && (
80 |
81 | {renderAccessory(props)}
82 |
83 | )}
84 |
85 | )
86 | }
87 |
88 | const styles = StyleSheet.create({
89 | container: {
90 | borderTopWidth: StyleSheet.hairlineWidth,
91 | borderTopColor: Color.defaultColor,
92 | backgroundColor: Color.white,
93 | },
94 | primary: {
95 | flexDirection: 'row',
96 | alignItems: 'flex-end',
97 | },
98 | accessory: {
99 | height: 44,
100 | },
101 | })
102 |
--------------------------------------------------------------------------------
/src/LoadEarlier.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | ActivityIndicator,
4 | Platform,
5 | StyleSheet,
6 | Text,
7 | TouchableOpacity,
8 | View,
9 | StyleProp,
10 | ViewStyle,
11 | TextStyle,
12 | } from 'react-native'
13 | import Color from './Color'
14 | import stylesCommon from './styles'
15 |
16 | const styles = StyleSheet.create({
17 | container: {
18 | alignItems: 'center',
19 | marginTop: 5,
20 | marginBottom: 10,
21 | },
22 | wrapper: {
23 | backgroundColor: Color.defaultColor,
24 | borderRadius: 15,
25 | height: 30,
26 | paddingLeft: 10,
27 | paddingRight: 10,
28 | },
29 | text: {
30 | backgroundColor: Color.backgroundTransparent,
31 | color: Color.white,
32 | fontSize: 12,
33 | },
34 | activityIndicator: {
35 | marginTop: Platform.select({
36 | ios: -14,
37 | android: -16,
38 | default: -15,
39 | }),
40 | },
41 | })
42 |
43 | export interface LoadEarlierProps {
44 | isLoadingEarlier?: boolean
45 | label?: string
46 | containerStyle?: StyleProp
47 | wrapperStyle?: StyleProp
48 | textStyle?: StyleProp
49 | activityIndicatorStyle?: StyleProp
50 | activityIndicatorColor?: string
51 | activityIndicatorSize?: number | 'small' | 'large'
52 | onLoadEarlier?(): void
53 | }
54 |
55 | export function LoadEarlier ({
56 | isLoadingEarlier = false,
57 | onLoadEarlier = () => {},
58 | label = 'Load earlier messages',
59 | containerStyle,
60 | wrapperStyle,
61 | textStyle,
62 | activityIndicatorColor = 'white',
63 | activityIndicatorSize = 'small',
64 | activityIndicatorStyle,
65 | }: LoadEarlierProps): React.ReactElement {
66 | return (
67 |
73 |
74 | {isLoadingEarlier
75 | ? (
76 |
77 |
78 | {label}
79 |
80 |
85 |
86 | )
87 | : (
88 | {label}
89 | )}
90 |
91 |
92 | )
93 | }
94 |
--------------------------------------------------------------------------------
/src/Message/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react'
2 | import { View } from 'react-native'
3 | import isEqual from 'lodash.isequal'
4 |
5 | import { Avatar } from '../Avatar'
6 | import Bubble from '../Bubble'
7 | import { SystemMessage } from '../SystemMessage'
8 |
9 | import { isSameUser } from '../utils'
10 | import { IMessage } from '../types'
11 | import { MessageProps } from './types'
12 | import styles from './styles'
13 |
14 | export * from './types'
15 |
16 | let Message: React.FC> = (props: MessageProps) => {
17 | const {
18 | currentMessage,
19 | renderBubble: renderBubbleProp,
20 | renderSystemMessage: renderSystemMessageProp,
21 | onMessageLayout,
22 | nextMessage,
23 | position,
24 | containerStyle,
25 | user,
26 | showUserAvatar,
27 | } = props
28 |
29 | const renderBubble = useCallback(() => {
30 | const {
31 | /* eslint-disable @typescript-eslint/no-unused-vars */
32 | containerStyle,
33 | onMessageLayout,
34 | /* eslint-enable @typescript-eslint/no-unused-vars */
35 | ...rest
36 | } = props
37 |
38 | if (renderBubbleProp)
39 | return renderBubbleProp(rest)
40 |
41 | return
42 | }, [props, renderBubbleProp])
43 |
44 | const renderSystemMessage = useCallback(() => {
45 | const {
46 | /* eslint-disable @typescript-eslint/no-unused-vars */
47 | containerStyle,
48 | onMessageLayout,
49 | /* eslint-enable @typescript-eslint/no-unused-vars */
50 | ...rest
51 | } = props
52 |
53 | if (renderSystemMessageProp)
54 | return renderSystemMessageProp(rest)
55 |
56 | return
57 | }, [props, renderSystemMessageProp])
58 |
59 | const renderAvatar = useCallback(() => {
60 | if (
61 | user?._id &&
62 | currentMessage?.user &&
63 | user._id === currentMessage.user._id &&
64 | !showUserAvatar
65 | )
66 | return null
67 |
68 | if (currentMessage?.user?.avatar === null)
69 | return null
70 |
71 | const {
72 | /* eslint-disable @typescript-eslint/no-unused-vars */
73 | containerStyle,
74 | onMessageLayout,
75 | /* eslint-enable @typescript-eslint/no-unused-vars */
76 | ...rest
77 | } = props
78 |
79 | return
80 | }, [
81 | props,
82 | user,
83 | currentMessage,
84 | showUserAvatar,
85 | ])
86 |
87 | if (!currentMessage)
88 | return null
89 |
90 | const sameUser = isSameUser(currentMessage, nextMessage!)
91 |
92 | return (
93 |
94 | {currentMessage.system
95 | ? (
96 | renderSystemMessage()
97 | )
98 | : (
99 |
107 | {position === 'left' ? renderAvatar() : null}
108 | {renderBubble()}
109 | {position === 'right' ? renderAvatar() : null}
110 |
111 | )}
112 |
113 | )
114 | }
115 |
116 | Message = memo(Message, (props, nextProps) => {
117 | const shouldUpdate =
118 | props.shouldUpdateMessage?.(props, nextProps) ||
119 | !isEqual(props.currentMessage!, nextProps.currentMessage!) ||
120 | !isEqual(props.previousMessage, nextProps.previousMessage) ||
121 | !isEqual(props.nextMessage, nextProps.nextMessage)
122 |
123 | if (shouldUpdate)
124 | return false
125 |
126 | return true
127 | })
128 |
129 | export default Message
130 |
--------------------------------------------------------------------------------
/src/Message/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | export default {
4 | left: StyleSheet.create({
5 | container: {
6 | flexDirection: 'row',
7 | alignItems: 'flex-end',
8 | justifyContent: 'flex-start',
9 | marginLeft: 8,
10 | marginRight: 0,
11 | },
12 | }),
13 | right: StyleSheet.create({
14 | container: {
15 | flexDirection: 'row',
16 | alignItems: 'flex-end',
17 | justifyContent: 'flex-end',
18 | marginLeft: 0,
19 | marginRight: 8,
20 | },
21 | }),
22 | }
23 |
--------------------------------------------------------------------------------
/src/Message/types.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle, LayoutChangeEvent } from 'react-native'
2 | import { AvatarProps } from '../Avatar'
3 | import { SystemMessageProps } from '../SystemMessage'
4 | import { DayProps } from '../Day'
5 | import { IMessage, User, LeftRightStyle } from '../types'
6 | import { BubbleProps } from '../Bubble'
7 |
8 | export interface MessageProps {
9 | showUserAvatar?: boolean
10 | position: 'left' | 'right'
11 | currentMessage: TMessage
12 | nextMessage?: TMessage
13 | previousMessage?: TMessage
14 | user: User
15 | inverted?: boolean
16 | containerStyle?: LeftRightStyle
17 | renderBubble?(props: BubbleProps): React.ReactNode
18 | renderDay?(props: DayProps): React.ReactNode
19 | renderSystemMessage?(props: SystemMessageProps): React.ReactNode
20 | renderAvatar?(props: AvatarProps): React.ReactNode
21 | shouldUpdateMessage?(
22 | props: MessageProps,
23 | nextProps: MessageProps,
24 | ): boolean
25 | onMessageLayout?(event: LayoutChangeEvent): void
26 | }
27 |
--------------------------------------------------------------------------------
/src/MessageAudio.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from './Color'
3 | import { View, Text } from 'react-native'
4 |
5 | export function MessageAudio () {
6 | return (
7 |
8 |
9 | {'Audio is not implemented by GiftedChat.'}
10 |
11 |
12 | {'\nYou need to provide your own implementation by using renderMessageAudio prop.'}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/MessageContainer/components/DayAnimated/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'
2 | import { LayoutChangeEvent } from 'react-native'
3 | import Animated, { interpolate, useAnimatedStyle, useDerivedValue, useSharedValue, useAnimatedReaction, withTiming, runOnJS } from 'react-native-reanimated'
4 | import { Day } from '../../../Day'
5 | import { isSameDay } from '../../../utils'
6 | import { useAbsoluteScrolledPositionToBottomOfDay, useRelativeScrolledPositionToBottomOfDay } from '../Item'
7 | import { DayAnimatedProps } from './types'
8 |
9 | import stylesCommon from '../../../styles'
10 | import styles from './styles'
11 |
12 | export * from './types'
13 |
14 | const DayAnimated = ({ scrolledY, daysPositions, listHeight, renderDay, messages, isLoadingEarlier, ...rest }: DayAnimatedProps) => {
15 | const opacity = useSharedValue(0)
16 | const fadeOutOpacityTimeoutId = useSharedValue | undefined>(undefined)
17 | const containerHeight = useSharedValue(0)
18 |
19 | const isScrolledOnMount = useSharedValue(false)
20 | const isLoadingEarlierAnim = useSharedValue(isLoadingEarlier)
21 |
22 | const daysPositionsArray = useDerivedValue(() => Object.values(daysPositions.value).sort((a, b) => a.y - b.y))
23 |
24 | const [createdAt, setCreatedAt] = useState()
25 |
26 | const dayTopOffset = useMemo(() => 10, [])
27 | const dayBottomMargin = useMemo(() => 10, [])
28 | const absoluteScrolledPositionToBottomOfDay = useAbsoluteScrolledPositionToBottomOfDay(listHeight, scrolledY, containerHeight, dayBottomMargin, dayTopOffset)
29 | const relativeScrolledPositionToBottomOfDay = useRelativeScrolledPositionToBottomOfDay(listHeight, scrolledY, daysPositions, containerHeight, dayBottomMargin, dayTopOffset)
30 |
31 | const messagesDates = useMemo(() => {
32 | const messagesDates: number[] = []
33 |
34 | for (let i = 1; i < messages.length; i++) {
35 | const previousMessage = messages[i - 1]
36 | const message = messages[i]
37 |
38 | if (!isSameDay(previousMessage, message) || !messagesDates.includes(new Date(message.createdAt).getTime()))
39 | messagesDates.push(new Date(message.createdAt).getTime())
40 | }
41 |
42 | return messagesDates
43 | }, [messages])
44 |
45 | const createdAtDate = useDerivedValue(() => {
46 | for (let i = 0; i < daysPositionsArray.value.length; i++) {
47 | const day = daysPositionsArray.value[i]
48 | const dayPosition = day.y + day.height - containerHeight.value - dayBottomMargin
49 |
50 | if (absoluteScrolledPositionToBottomOfDay.value < dayPosition)
51 | return day.createdAt
52 | }
53 |
54 | return messagesDates[messagesDates.length - 1]
55 | }, [daysPositionsArray, absoluteScrolledPositionToBottomOfDay, messagesDates, containerHeight, dayBottomMargin])
56 |
57 | const style = useAnimatedStyle(() => ({
58 | top: interpolate(
59 | relativeScrolledPositionToBottomOfDay.value,
60 | [-dayTopOffset, -0.0001, 0, isLoadingEarlierAnim.value ? 0 : containerHeight.value + dayTopOffset],
61 | [dayTopOffset, dayTopOffset, -containerHeight.value, isLoadingEarlierAnim.value ? -containerHeight.value : dayTopOffset],
62 | 'clamp'
63 | ),
64 | }), [relativeScrolledPositionToBottomOfDay, containerHeight, dayTopOffset, isLoadingEarlierAnim])
65 |
66 | const contentStyle = useAnimatedStyle(() => ({
67 | opacity: opacity.value,
68 | }), [opacity])
69 |
70 | const fadeOut = useCallback(() => {
71 | 'worklet'
72 |
73 | opacity.value = withTiming(0, { duration: 500 })
74 | }, [opacity])
75 |
76 | const scheduleFadeOut = useCallback(() => {
77 | clearTimeout(fadeOutOpacityTimeoutId.value)
78 |
79 | fadeOutOpacityTimeoutId.value = setTimeout(fadeOut, 500)
80 | }, [fadeOut, fadeOutOpacityTimeoutId])
81 |
82 | const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
83 | containerHeight.value = nativeEvent.layout.height
84 | }, [containerHeight])
85 |
86 | useAnimatedReaction(
87 | () => [scrolledY.value, daysPositionsArray],
88 | (value, prevValue) => {
89 | if (!isScrolledOnMount.value) {
90 | isScrolledOnMount.value = true
91 | return
92 | }
93 |
94 | if (value[0] === prevValue?.[0])
95 | return
96 |
97 | opacity.value = withTiming(1, { duration: 500 })
98 |
99 | runOnJS(scheduleFadeOut)()
100 | },
101 | [scrolledY, scheduleFadeOut, daysPositionsArray]
102 | )
103 |
104 | useAnimatedReaction(
105 | () => createdAtDate.value,
106 | (value, prevValue) => {
107 | if (value && value !== prevValue)
108 | runOnJS(setCreatedAt)(value)
109 | },
110 | [createdAtDate]
111 | )
112 |
113 | useEffect(() => {
114 | isLoadingEarlierAnim.value = isLoadingEarlier
115 | }, [isLoadingEarlierAnim, isLoadingEarlier])
116 |
117 | if (!createdAt)
118 | return null
119 |
120 | return (
121 |
125 |
129 | {
130 | renderDay
131 | ? renderDay({ ...rest, createdAt })
132 | :
137 | }
138 |
139 |
140 | )
141 | }
142 |
143 | export default DayAnimated
144 |
--------------------------------------------------------------------------------
/src/MessageContainer/components/DayAnimated/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | export default StyleSheet.create({
4 | dayAnimated: {
5 | position: 'absolute',
6 | width: '100%',
7 | },
8 | dayAnimatedDayContainerStyle: {
9 | marginTop: 0,
10 | marginBottom: 0,
11 | },
12 | })
13 |
--------------------------------------------------------------------------------
/src/MessageContainer/components/DayAnimated/types.ts:
--------------------------------------------------------------------------------
1 | import { DayProps } from '../../../Day'
2 | import { IMessage } from '../../../types'
3 | import { DaysPositions } from '../../types'
4 |
5 | export interface DayAnimatedProps extends Omit {
6 | scrolledY: { value: number }
7 | daysPositions: { value: DaysPositions }
8 | listHeight: { value: number }
9 | renderDay?: (props: DayProps) => React.ReactNode
10 | messages: IMessage[]
11 | isLoadingEarlier: boolean
12 | }
13 |
--------------------------------------------------------------------------------
/src/MessageContainer/components/Item/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useCallback, useMemo } from 'react'
2 | import { LayoutChangeEvent, View } from 'react-native'
3 | import { IMessage } from '../../../types'
4 | import Message, { MessageProps } from '../../../Message'
5 | import Animated, { interpolate, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated'
6 | import { DaysPositions } from '../../types'
7 | import { Day } from '../../../Day'
8 | import { isSameDay } from '../../../utils'
9 | import { ItemProps } from './types'
10 |
11 | export * from './types'
12 |
13 | // y-position of current scroll position relative to the bottom of the day container. (since we have inverted list it is bottom)
14 | export const useAbsoluteScrolledPositionToBottomOfDay = (listHeight: { value: number }, scrolledY: { value: number }, containerHeight: { value: number }, dayBottomMargin: number, dayTopOffset: number) => {
15 | const absoluteScrolledPositionToBottomOfDay = useDerivedValue(() =>
16 | listHeight.value + scrolledY.value - containerHeight.value - dayBottomMargin - dayTopOffset
17 | , [listHeight, scrolledY, containerHeight, dayBottomMargin, dayTopOffset])
18 |
19 | return absoluteScrolledPositionToBottomOfDay
20 | }
21 |
22 | export const useRelativeScrolledPositionToBottomOfDay = (
23 | listHeight: { value: number },
24 | scrolledY: { value: number },
25 | daysPositions: { value: DaysPositions },
26 | containerHeight: { value: number },
27 | dayBottomMargin: number,
28 | dayTopOffset: number,
29 | createdAt?: number
30 | ) => {
31 | const dayMarginTop = useMemo(() => 5, [])
32 |
33 | const absoluteScrolledPositionToBottomOfDay = useAbsoluteScrolledPositionToBottomOfDay(listHeight, scrolledY, containerHeight, dayBottomMargin, dayTopOffset)
34 |
35 | // sorted array of days positions by y
36 | const daysPositionsArray = useDerivedValue(() => Object.values(daysPositions.value).sort((a, b) => a.y - b.y))
37 |
38 | // find current day position by scrolled position
39 | const currentDayPosition = useDerivedValue(() => {
40 | if (createdAt != null) {
41 | const currentDayPosition = daysPositionsArray.value.find(day => day.createdAt === createdAt)
42 | if (currentDayPosition)
43 | return currentDayPosition
44 | }
45 |
46 | return daysPositionsArray.value.find((day, index) => {
47 | const dayPosition = day.y + day.height
48 | return (absoluteScrolledPositionToBottomOfDay.value < dayPosition) || index === daysPositionsArray.value.length - 1
49 | })
50 | }, [daysPositionsArray, absoluteScrolledPositionToBottomOfDay, createdAt])
51 |
52 | const relativeScrolledPositionToBottomOfDay = useDerivedValue(() => {
53 | const scrolledBottomY = listHeight.value + scrolledY.value - (
54 | (currentDayPosition.value?.y ?? 0) +
55 | (currentDayPosition.value?.height ?? 0) +
56 | dayMarginTop
57 | )
58 |
59 | return scrolledBottomY
60 | }, [listHeight, scrolledY, currentDayPosition, dayMarginTop])
61 |
62 | return relativeScrolledPositionToBottomOfDay
63 | }
64 |
65 | const DayWrapper = forwardRef>((props, ref) => {
66 | const {
67 | renderDay: renderDayProp,
68 | currentMessage,
69 | previousMessage,
70 | } = props
71 |
72 | if (!currentMessage?.createdAt || isSameDay(currentMessage, previousMessage))
73 | return null
74 |
75 | const {
76 | /* eslint-disable @typescript-eslint/no-unused-vars */
77 | containerStyle,
78 | onMessageLayout,
79 | /* eslint-enable @typescript-eslint/no-unused-vars */
80 | ...rest
81 | } = props
82 |
83 | return (
84 |
85 | {
86 | renderDayProp
87 | ? renderDayProp({ ...rest, createdAt: currentMessage.createdAt })
88 | :
89 | }
90 |
91 | )
92 | })
93 |
94 | const Item = (props: ItemProps) => {
95 | const {
96 | renderMessage: renderMessageProp,
97 | scrolledY,
98 | daysPositions,
99 | listHeight,
100 | ...rest
101 | } = props
102 |
103 | const dayContainerHeight = useSharedValue(0)
104 | const dayTopOffset = useMemo(() => 10, [])
105 | const dayBottomMargin = useMemo(() => 10, [])
106 |
107 | const createdAt = useMemo(() =>
108 | new Date(props.currentMessage.createdAt).getTime()
109 | , [props.currentMessage.createdAt])
110 |
111 | const relativeScrolledPositionToBottomOfDay = useRelativeScrolledPositionToBottomOfDay(listHeight, scrolledY, daysPositions, dayContainerHeight, dayBottomMargin, dayTopOffset, createdAt)
112 |
113 | const handleLayoutDayContainer = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
114 | dayContainerHeight.value = nativeEvent.layout.height
115 | }, [dayContainerHeight])
116 |
117 | const style = useAnimatedStyle(() => ({
118 | opacity: interpolate(
119 | relativeScrolledPositionToBottomOfDay.value,
120 | [
121 | -dayTopOffset,
122 | -0.0001,
123 | 0,
124 | dayContainerHeight.value + dayTopOffset,
125 | ],
126 | [
127 | 0,
128 | 0,
129 | 1,
130 | 1,
131 | ],
132 | 'clamp'
133 | ),
134 | }), [relativeScrolledPositionToBottomOfDay, dayContainerHeight, dayTopOffset])
135 |
136 | return (
137 | // do not remove key. it helps to get correct position of the day container
138 |
139 |
143 | } />
144 |
145 | {
146 | renderMessageProp
147 | ? renderMessageProp(rest as MessageProps)
148 | : } />
149 | }
150 |
151 | )
152 | }
153 |
154 | export default Item
155 |
--------------------------------------------------------------------------------
/src/MessageContainer/components/Item/types.ts:
--------------------------------------------------------------------------------
1 | import { MessageContainerProps, DaysPositions } from '../../types'
2 | import { IMessage } from '../../../types'
3 |
4 | export interface ItemProps extends MessageContainerProps {
5 | currentMessage: TMessage
6 | previousMessage?: TMessage
7 | nextMessage?: TMessage
8 | position: 'left' | 'right'
9 | scrolledY: { value: number }
10 | daysPositions: { value: DaysPositions }
11 | listHeight: { value: number }
12 | }
13 |
--------------------------------------------------------------------------------
/src/MessageContainer/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 | import Color from '../Color'
3 |
4 | export default StyleSheet.create({
5 | containerAlignTop: {
6 | flexDirection: 'row',
7 | alignItems: 'flex-start',
8 | },
9 | contentContainerStyle: {
10 | flexGrow: 1,
11 | justifyContent: 'flex-start',
12 | },
13 | emptyChatContainer: {
14 | transform: [{ scaleY: -1 }],
15 | },
16 | scrollToBottomStyle: {
17 | opacity: 0.8,
18 | position: 'absolute',
19 | right: 10,
20 | bottom: 30,
21 | zIndex: 999,
22 | height: 40,
23 | width: 40,
24 | borderRadius: 20,
25 | backgroundColor: Color.white,
26 | shadowColor: Color.black,
27 | shadowOpacity: 0.5,
28 | shadowOffset: { width: 0, height: 0 },
29 | shadowRadius: 1,
30 | },
31 | })
32 |
--------------------------------------------------------------------------------
/src/MessageContainer/types.ts:
--------------------------------------------------------------------------------
1 | import React, { Component, RefObject } from 'react'
2 | import {
3 | FlatListProps,
4 | LayoutChangeEvent,
5 | StyleProp,
6 | ViewStyle,
7 | } from 'react-native'
8 |
9 | import { LoadEarlierProps } from '../LoadEarlier'
10 | import { MessageProps } from '../Message'
11 | import { User, IMessage, Reply } from '../types'
12 | import { ReanimatedScrollEvent } from 'react-native-reanimated/lib/typescript/hook/commonTypes'
13 | import { FlatList } from 'react-native-reanimated/lib/typescript/Animated'
14 | import { AnimateProps } from 'react-native-reanimated'
15 |
16 | export type ListViewProps = {
17 | onLayout?: (event: LayoutChangeEvent) => void
18 | } & object
19 |
20 | export type AnimatedList = Component>, unknown, unknown> & FlatList>
21 |
22 | export interface MessageContainerProps {
23 | forwardRef?: RefObject>
24 | messages?: TMessage[]
25 | isTyping?: boolean
26 | user?: User
27 | listViewProps?: ListViewProps
28 | inverted?: boolean
29 | loadEarlier?: boolean
30 | alignTop?: boolean
31 | isScrollToBottomEnabled?: boolean
32 | scrollToBottomStyle?: StyleProp
33 | invertibleScrollViewProps?: object
34 | extraData?: object
35 | scrollToBottomOffset?: number
36 | renderChatEmpty?(): React.ReactNode
37 | renderFooter?(props: MessageContainerProps): React.ReactNode
38 | renderMessage?(props: MessageProps): React.ReactElement
39 | renderLoadEarlier?(props: LoadEarlierProps): React.ReactNode
40 | renderTypingIndicator?(): React.ReactNode
41 | scrollToBottomComponent?(): React.ReactNode
42 | onLoadEarlier?(): void
43 | onQuickReply?(replies: Reply[]): void
44 | infiniteScroll?: boolean
45 | isLoadingEarlier?: boolean
46 | handleOnScroll?(event: ReanimatedScrollEvent): void
47 | }
48 |
49 | export interface State {
50 | showScrollBottom: boolean
51 | hasScrolled: boolean
52 | }
53 |
54 | interface ViewLayout {
55 | x: number
56 | y: number
57 | width: number
58 | height: number
59 | }
60 |
61 | export type DaysPositions = { [key: string]: ViewLayout & { createdAt: number } }
62 |
--------------------------------------------------------------------------------
/src/MessageImage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Image,
4 | StyleSheet,
5 | View,
6 | ImageProps,
7 | ViewStyle,
8 | StyleProp,
9 | ImageStyle,
10 | ImageURISource,
11 | } from 'react-native'
12 | // TODO: support web
13 | import Lightbox, { LightboxProps } from 'react-native-lightbox-v2'
14 | import { IMessage } from './types'
15 | import stylesCommon from './styles'
16 |
17 | const styles = StyleSheet.create({
18 | image: {
19 | width: 150,
20 | height: 100,
21 | borderRadius: 13,
22 | margin: 3,
23 | resizeMode: 'cover',
24 | },
25 | imageActive: {
26 | resizeMode: 'contain',
27 | },
28 | })
29 |
30 | export interface MessageImageProps {
31 | currentMessage: TMessage
32 | containerStyle?: StyleProp
33 | imageSourceProps?: Partial
34 | imageStyle?: StyleProp
35 | imageProps?: Partial
36 | lightboxProps?: LightboxProps
37 | }
38 |
39 | export function MessageImage ({
40 | containerStyle,
41 | lightboxProps,
42 | imageProps,
43 | imageSourceProps,
44 | imageStyle,
45 | currentMessage,
46 | }: MessageImageProps) {
47 | if (currentMessage == null)
48 | return null
49 |
50 | return (
51 |
52 | {/* @ts-expect-error: Lightbox types are not fully compatible */}
53 |
59 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/MessageText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Linking,
4 | StyleSheet,
5 | View,
6 | TextProps,
7 | StyleProp,
8 | ViewStyle,
9 | TextStyle,
10 | } from 'react-native'
11 |
12 | import ParsedText from 'react-native-parsed-text'
13 | import { LeftRightStyle, IMessage } from './types'
14 | import { useChatContext } from './GiftedChatContext'
15 | import { error } from './logging'
16 |
17 | const WWW_URL_PATTERN = /^www\./i
18 |
19 | const { textStyle } = StyleSheet.create({
20 | textStyle: {
21 | fontSize: 16,
22 | lineHeight: 20,
23 | marginTop: 5,
24 | marginBottom: 5,
25 | marginLeft: 10,
26 | marginRight: 10,
27 | },
28 | })
29 |
30 | const styles = {
31 | left: StyleSheet.create({
32 | container: {},
33 | text: {
34 | color: 'black',
35 | ...textStyle,
36 | },
37 | link: {
38 | color: 'black',
39 | textDecorationLine: 'underline',
40 | },
41 | }),
42 | right: StyleSheet.create({
43 | container: {},
44 | text: {
45 | color: 'white',
46 | ...textStyle,
47 | },
48 | link: {
49 | color: 'white',
50 | textDecorationLine: 'underline',
51 | },
52 | }),
53 | }
54 |
55 | const DEFAULT_OPTION_TITLES = ['Call', 'Text', 'Cancel']
56 |
57 | export interface MessageTextProps {
58 | position?: 'left' | 'right'
59 | optionTitles?: string[]
60 | currentMessage: TMessage
61 | containerStyle?: LeftRightStyle
62 | textStyle?: LeftRightStyle
63 | linkStyle?: LeftRightStyle
64 | textProps?: TextProps
65 | customTextStyle?: StyleProp
66 | parsePatterns?: (linkStyle: TextStyle) => []
67 | }
68 |
69 | export function MessageText ({
70 | currentMessage = {} as TMessage,
71 | optionTitles = DEFAULT_OPTION_TITLES,
72 | position = 'left',
73 | containerStyle,
74 | textStyle,
75 | linkStyle: linkStyleProp,
76 | customTextStyle,
77 | parsePatterns,
78 | textProps,
79 | }: MessageTextProps) {
80 | const { actionSheet } = useChatContext()
81 |
82 | // TODO: React.memo
83 | // const shouldComponentUpdate = (nextProps: MessageTextProps) => {
84 | // return (
85 | // !!currentMessage &&
86 | // !!nextProps.currentMessage &&
87 | // currentMessage.text !== nextProps.currentMessage.text
88 | // )
89 | // }
90 |
91 | const onUrlPress = (url: string) => {
92 | // When someone sends a message that includes a website address beginning with "www." (omitting the scheme),
93 | // react-native-parsed-text recognizes it as a valid url, but Linking fails to open due to the missing scheme.
94 | if (WWW_URL_PATTERN.test(url))
95 | onUrlPress(`https://${url}`)
96 | else
97 | Linking.openURL(url).catch(e => {
98 | error(e, 'No handler for URL:', url)
99 | })
100 | }
101 |
102 | const onPhonePress = (phone: string) => {
103 | const options =
104 | optionTitles && optionTitles.length > 0
105 | ? optionTitles.slice(0, 3)
106 | : DEFAULT_OPTION_TITLES
107 | const cancelButtonIndex = options.length - 1
108 | actionSheet().showActionSheetWithOptions(
109 | {
110 | options,
111 | cancelButtonIndex,
112 | },
113 | (buttonIndex?: number) => {
114 | switch (buttonIndex) {
115 | case 0:
116 | Linking.openURL(`tel:${phone}`).catch(e => {
117 | error(e, 'No handler for telephone')
118 | })
119 | break
120 | case 1:
121 | Linking.openURL(`sms:${phone}`).catch(e => {
122 | error(e, 'No handler for text')
123 | })
124 | break
125 | }
126 | }
127 | )
128 | }
129 |
130 | const onEmailPress = (email: string) =>
131 | Linking.openURL(`mailto:${email}`).catch(e =>
132 | error(e, 'No handler for mailto')
133 | )
134 |
135 | const linkStyle = [
136 | styles[position].link,
137 | linkStyleProp?.[position],
138 | ]
139 | return (
140 |
146 |
160 | {currentMessage!.text}
161 |
162 |
163 | )
164 | }
165 |
--------------------------------------------------------------------------------
/src/MessageVideo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Color from './Color'
3 | import { View, Text } from 'react-native'
4 |
5 | export function MessageVideo () {
6 | return (
7 |
8 |
9 | {'Video is not implemented by GiftedChat.'}
10 |
11 |
12 | {'\nYou need to provide your own implementation by using renderMessageVideo prop.'}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/QuickReplies.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useMemo, useCallback } from 'react'
2 | import {
3 | Text,
4 | StyleSheet,
5 | View,
6 | TouchableOpacity,
7 | StyleProp,
8 | ViewStyle,
9 | TextStyle,
10 | } from 'react-native'
11 | import { IMessage, Reply } from './types'
12 | import Color from './Color'
13 | import { warning } from './logging'
14 | import stylesCommon from './styles'
15 |
16 | const styles = StyleSheet.create({
17 | container: {
18 | flexDirection: 'row',
19 | flexWrap: 'wrap',
20 | maxWidth: 300,
21 | },
22 | quickReply: {
23 | borderWidth: 1,
24 | maxWidth: 200,
25 | paddingVertical: 7,
26 | paddingHorizontal: 12,
27 | minHeight: 50,
28 | borderRadius: 13,
29 | margin: 3,
30 | },
31 | quickReplyText: {
32 | overflow: 'visible',
33 | },
34 | sendLink: {
35 | borderWidth: 0,
36 | },
37 | sendLinkText: {
38 | color: Color.defaultBlue,
39 | fontWeight: '600',
40 | fontSize: 17,
41 | },
42 | })
43 |
44 | export interface QuickRepliesProps {
45 | nextMessage?: TMessage
46 | currentMessage: TMessage
47 | color?: string
48 | sendText?: string
49 | quickReplyStyle?: StyleProp
50 | quickReplyTextStyle?: StyleProp
51 | quickReplyContainerStyle?: StyleProp
52 | onQuickReply?(reply: Reply[]): void
53 | renderQuickReplySend?(): React.ReactNode
54 | }
55 |
56 | const sameReply = (currentReply: Reply) => (reply: Reply) =>
57 | currentReply.value === reply.value
58 |
59 | const diffReply = (currentReply: Reply) => (reply: Reply) =>
60 | currentReply.value !== reply.value
61 |
62 | export function QuickReplies ({
63 | currentMessage,
64 | nextMessage,
65 | color = Color.peterRiver,
66 | quickReplyStyle,
67 | quickReplyTextStyle,
68 | quickReplyContainerStyle,
69 | onQuickReply,
70 | sendText = 'Send',
71 | renderQuickReplySend,
72 | }: QuickRepliesProps) {
73 | const { type } = currentMessage!.quickReplies!
74 | const [replies, setReplies] = useState([])
75 |
76 | const shouldComponentDisplay = useMemo(() => {
77 | const hasReplies = !!currentMessage && !!currentMessage!.quickReplies
78 | const hasNext = !!nextMessage && !!nextMessage!._id
79 | const keepIt = currentMessage!.quickReplies!.keepIt
80 |
81 | if (hasReplies && !hasNext)
82 | return true
83 |
84 | if (hasReplies && hasNext && keepIt)
85 | return true
86 |
87 | return false
88 | }, [currentMessage, nextMessage])
89 |
90 | const handleSend = useCallback((repliesData: Reply[]) => () => {
91 | onQuickReply?.(
92 | repliesData.map((reply: Reply) => ({
93 | ...reply,
94 | messageId: currentMessage!._id,
95 | }))
96 | )
97 | }, [onQuickReply, currentMessage])
98 |
99 | const handlePress = useCallback(
100 | (reply: Reply) => () => {
101 | if (currentMessage) {
102 | const { type } = currentMessage.quickReplies!
103 | switch (type) {
104 | case 'radio': {
105 | handleSend([reply])()
106 | return
107 | }
108 | case 'checkbox': {
109 | if (replies.find(sameReply(reply)))
110 | setReplies(replies.filter(diffReply(reply)))
111 | else
112 | setReplies([...replies, reply])
113 |
114 | return
115 | }
116 | default: {
117 | warning(`onQuickReply unknown type: ${type}`)
118 | }
119 | }
120 | }
121 | },
122 | [replies, currentMessage, handleSend]
123 | )
124 |
125 | if (!shouldComponentDisplay)
126 | return null
127 |
128 | return (
129 |
130 | {currentMessage!.quickReplies!.values.map(
131 | (reply: Reply, index: number) => {
132 | const selected =
133 | type === 'checkbox' && replies.find(sameReply(reply))
134 |
135 | return (
136 |
147 |
156 | {reply.title}
157 |
158 |
159 | )
160 | }
161 | )}
162 | {replies.length > 0 && (
163 |
167 | {renderQuickReplySend?.() || (
168 | {sendText}
169 | )}
170 |
171 | )}
172 |
173 | )
174 | }
175 |
--------------------------------------------------------------------------------
/src/Send.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useCallback } from 'react'
2 | import {
3 | StyleSheet,
4 | Text,
5 | TouchableOpacity,
6 | View,
7 | StyleProp,
8 | ViewStyle,
9 | TextStyle,
10 | TouchableOpacityProps,
11 | } from 'react-native'
12 |
13 | import Color from './Color'
14 | import { IMessage } from './types'
15 | import { TEST_ID } from './Constant'
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | height: 44,
20 | justifyContent: 'flex-end',
21 | },
22 | text: {
23 | color: Color.defaultBlue,
24 | fontWeight: '600',
25 | fontSize: 17,
26 | backgroundColor: Color.backgroundTransparent,
27 | marginBottom: 12,
28 | marginLeft: 10,
29 | marginRight: 10,
30 | },
31 | })
32 |
33 | export interface SendProps {
34 | text?: string
35 | label?: string
36 | containerStyle?: StyleProp
37 | textStyle?: StyleProp
38 | children?: React.ReactNode
39 | alwaysShowSend?: boolean
40 | disabled?: boolean
41 | sendButtonProps?: Partial
42 | onSend?(
43 | messages: Partial | Partial[],
44 | shouldResetInputToolbar: boolean,
45 | ): void
46 | }
47 |
48 | export const Send = ({
49 | text,
50 | containerStyle,
51 | children,
52 | textStyle,
53 | label = 'Send',
54 | alwaysShowSend = false,
55 | disabled = false,
56 | sendButtonProps,
57 | onSend,
58 | }: SendProps) => {
59 | const handleOnPress = useCallback(() => {
60 | if (text && onSend)
61 | onSend({ text: text.trim() } as Partial, true)
62 | }, [text, onSend])
63 |
64 | const showSend = useMemo(
65 | () => alwaysShowSend || (text && text.trim().length > 0),
66 | [alwaysShowSend, text]
67 | )
68 |
69 | if (!showSend)
70 | return null
71 |
72 | return (
73 |
83 |
84 | {children || {label}}
85 |
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/SystemMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | StyleSheet,
4 | Text,
5 | View,
6 | ViewStyle,
7 | StyleProp,
8 | TextStyle,
9 | } from 'react-native'
10 | import Color from './Color'
11 | import { IMessage } from './types'
12 | import stylesCommon from './styles'
13 |
14 | const styles = StyleSheet.create({
15 | container: {
16 | marginTop: 5,
17 | marginBottom: 10,
18 | },
19 | text: {
20 | backgroundColor: Color.backgroundTransparent,
21 | color: Color.defaultColor,
22 | fontSize: 12,
23 | fontWeight: '300',
24 | },
25 | })
26 |
27 | export interface SystemMessageProps {
28 | currentMessage: TMessage
29 | containerStyle?: StyleProp
30 | wrapperStyle?: StyleProp
31 | textStyle?: StyleProp
32 | children?: React.ReactNode
33 | }
34 |
35 | export function SystemMessage ({
36 | currentMessage,
37 | containerStyle,
38 | wrapperStyle,
39 | textStyle,
40 | children,
41 | }: SystemMessageProps) {
42 | if (currentMessage == null || currentMessage.system === false)
43 | return null
44 |
45 | return (
46 |
47 |
48 | {!!currentMessage.text && {currentMessage.text}}
49 | {children}
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/Time.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native'
3 | import dayjs from 'dayjs'
4 |
5 | import Color from './Color'
6 | import { TIME_FORMAT } from './Constant'
7 | import { LeftRightStyle, IMessage } from './types'
8 | import { useChatContext } from './GiftedChatContext'
9 |
10 | const { containerStyle } = StyleSheet.create({
11 | containerStyle: {
12 | marginLeft: 10,
13 | marginRight: 10,
14 | marginBottom: 5,
15 | },
16 | })
17 |
18 | const { textStyle } = StyleSheet.create({
19 | textStyle: {
20 | fontSize: 10,
21 | textAlign: 'right',
22 | },
23 | })
24 |
25 | const styles = {
26 | left: StyleSheet.create({
27 | container: {
28 | ...containerStyle,
29 | },
30 | text: {
31 | color: Color.timeTextColor,
32 | ...textStyle,
33 | },
34 | }),
35 | right: StyleSheet.create({
36 | container: {
37 | ...containerStyle,
38 | },
39 | text: {
40 | color: Color.white,
41 | ...textStyle,
42 | },
43 | }),
44 | }
45 |
46 | export interface TimeProps {
47 | position?: 'left' | 'right'
48 | currentMessage: TMessage
49 | containerStyle?: LeftRightStyle
50 | timeTextStyle?: LeftRightStyle
51 | timeFormat?: string
52 | }
53 |
54 | export function Time ({
55 | position = 'left',
56 | containerStyle,
57 | currentMessage,
58 | timeFormat = TIME_FORMAT,
59 | timeTextStyle,
60 | }: TimeProps) {
61 | const { getLocale } = useChatContext()
62 |
63 | if (currentMessage == null)
64 | return null
65 |
66 | return (
67 |
73 |
79 | {dayjs(currentMessage.createdAt).locale(getLocale()).format(timeFormat)}
80 |
81 |
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/src/TypingIndicator/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState, useMemo } from 'react'
2 | import { View } from 'react-native'
3 | import Animated, {
4 | runOnJS,
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withDelay,
8 | withRepeat,
9 | withSequence,
10 | withTiming,
11 | } from 'react-native-reanimated'
12 | import { TypingIndicatorProps } from './types'
13 |
14 | import stylesCommon from '../styles'
15 | import styles from './styles'
16 |
17 | export * from './types'
18 |
19 | const DotsAnimation = () => {
20 | const dot1 = useSharedValue(0)
21 | const dot2 = useSharedValue(0)
22 | const dot3 = useSharedValue(0)
23 |
24 | const topY = useMemo(() => -5, [])
25 | const bottomY = useMemo(() => 5, [])
26 | const duration = useMemo(() => 500, [])
27 |
28 | const dot1Style = useAnimatedStyle(() => ({
29 | transform: [{
30 | translateY: dot1.value,
31 | }],
32 | }), [dot1])
33 |
34 | const dot2Style = useAnimatedStyle(() => ({
35 | transform: [{
36 | translateY: dot2.value,
37 | }],
38 | }), [dot2])
39 |
40 | const dot3Style = useAnimatedStyle(() => ({
41 | transform: [{
42 | translateY: dot3.value,
43 | }],
44 | }), [dot3])
45 |
46 | useEffect(() => {
47 | dot1.value = withRepeat(
48 | withSequence(
49 | withTiming(topY, { duration }),
50 | withTiming(bottomY, { duration })
51 | ),
52 | 0,
53 | true
54 | )
55 | }, [dot1, topY, bottomY, duration])
56 |
57 | useEffect(() => {
58 | dot2.value = withDelay(100,
59 | withRepeat(
60 | withSequence(
61 | withTiming(topY, { duration }),
62 | withTiming(bottomY, { duration })
63 | ),
64 | 0,
65 | true
66 | )
67 | )
68 | }, [dot2, topY, bottomY, duration])
69 |
70 | useEffect(() => {
71 | dot3.value = withDelay(200,
72 | withRepeat(
73 | withSequence(
74 | withTiming(topY, { duration }),
75 | withTiming(bottomY, { duration })
76 | ),
77 | 0,
78 | true
79 | )
80 | )
81 | }, [dot3, topY, bottomY, duration])
82 |
83 | return (
84 |
85 |
86 |
87 |
88 |
89 | )
90 | }
91 |
92 | const TypingIndicator = ({ isTyping }: TypingIndicatorProps) => {
93 | const yCoords = useSharedValue(200)
94 | const heightScale = useSharedValue(0)
95 | const marginScale = useSharedValue(0)
96 |
97 | const [isVisible, setIsVisible] = useState(isTyping)
98 |
99 | const containerStyle = useAnimatedStyle(() => ({
100 | transform: [
101 | {
102 | translateY: yCoords.value,
103 | },
104 | ],
105 | height: heightScale.value,
106 | marginBottom: marginScale.value,
107 | }), [yCoords, heightScale, marginScale])
108 |
109 | const slideIn = useCallback(() => {
110 | const duration = 250
111 |
112 | yCoords.value = withTiming(0, { duration })
113 | heightScale.value = withTiming(35, { duration })
114 | marginScale.value = withTiming(8, { duration })
115 | }, [yCoords, heightScale, marginScale])
116 |
117 | const slideOut = useCallback(() => {
118 | const duration = 250
119 |
120 | yCoords.value = withTiming(200, { duration }, isFinished => {
121 | if (isFinished)
122 | runOnJS(setIsVisible)(false)
123 | })
124 | heightScale.value = withTiming(0, { duration })
125 | marginScale.value = withTiming(0, { duration })
126 | }, [yCoords, heightScale, marginScale])
127 |
128 | useEffect(() => {
129 | if (isVisible)
130 | if (isTyping)
131 | slideIn()
132 | else
133 | slideOut()
134 | }, [isVisible, isTyping, slideIn, slideOut])
135 |
136 | useEffect(() => {
137 | if (isTyping)
138 | setIsVisible(true)
139 | }, [isTyping])
140 |
141 | if (!isVisible)
142 | return null
143 |
144 | return (
145 |
151 |
152 |
153 | )
154 | }
155 |
156 | export default TypingIndicator
157 |
--------------------------------------------------------------------------------
/src/TypingIndicator/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 | import Color from '../Color'
3 |
4 | export default StyleSheet.create({
5 | container: {
6 | marginLeft: 8,
7 | width: 45,
8 | borderRadius: 15,
9 | backgroundColor: Color.leftBubbleBackground,
10 | },
11 | dots: {
12 | flexDirection: 'row',
13 | },
14 | dot: {
15 | marginLeft: 2,
16 | marginRight: 2,
17 | borderRadius: 4,
18 | width: 8,
19 | height: 8,
20 | backgroundColor: 'rgba(0, 0, 0, 0.38)',
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/src/TypingIndicator/types.ts:
--------------------------------------------------------------------------------
1 | export interface TypingIndicatorProps {
2 | isTyping?: boolean
3 | }
4 |
--------------------------------------------------------------------------------
/src/__tests__/Actions.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Actions } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 | expect(tree).toMatchSnapshot()
10 | })
11 |
--------------------------------------------------------------------------------
/src/__tests__/Avatar.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Avatar } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer
9 | .create( 'renderAvatar'} position='left' />)
10 | .toJSON()
11 |
12 | expect(tree).toMatchSnapshot()
13 | })
14 |
--------------------------------------------------------------------------------
/src/__tests__/Bubble.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Bubble } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | let tree
9 |
10 | renderer.act(() => {
11 | tree = renderer.create(
12 |
22 | )
23 | })
24 |
25 | expect(tree.toJSON()).toMatchSnapshot()
26 | })
27 |
--------------------------------------------------------------------------------
/src/__tests__/Color.test.tsx:
--------------------------------------------------------------------------------
1 | import Color from '../Color'
2 |
3 | it('should compare Color with snapshot', () => {
4 | expect(Color).toMatchSnapshot()
5 | })
6 |
--------------------------------------------------------------------------------
/src/__tests__/Composer.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Composer } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 |
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/src/__tests__/Constant.test.tsx:
--------------------------------------------------------------------------------
1 | import * as Constant from '../Constant'
2 |
3 | it('should compare Constant with snapshot', () => {
4 | expect(Constant).toMatchSnapshot()
5 | })
6 |
--------------------------------------------------------------------------------
/src/__tests__/Day.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Day } from '../GiftedChat'
6 | import { DEFAULT_TEST_MESSAGE } from './data'
7 |
8 | describe('Day', () => {
9 | it('should not render and compare with snapshot', () => {
10 | const component = renderer.create()
11 | const tree = component.toJSON()
12 |
13 | expect(tree).toMatchSnapshot()
14 | })
15 |
16 | it('should render and compare with snapshot', () => {
17 | const component = renderer.create(
18 |
19 | )
20 | const tree = component.toJSON()
21 | expect(tree).toMatchSnapshot()
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/src/__tests__/GiftedAvatar.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { GiftedAvatar } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | let tree
9 |
10 | renderer.act(() => {
11 | tree = renderer.create()
12 | })
13 |
14 | expect(tree.toJSON()).toMatchSnapshot()
15 | })
16 |
--------------------------------------------------------------------------------
/src/__tests__/GiftedChat.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { GiftedChat } from '../GiftedChat'
6 | import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'
7 |
8 | const messages = [
9 | {
10 | _id: 1,
11 | text: 'Hello developer',
12 | createdAt: new Date(),
13 | user: {
14 | _id: 2,
15 | name: 'React Native',
16 | },
17 | },
18 | ]
19 |
20 | it('should render and compare with snapshot', () => {
21 | let tree
22 |
23 | renderer.act(() => {
24 | (useReanimatedKeyboardAnimation as jest.Mock).mockReturnValue({
25 | height: {
26 | value: 0,
27 | },
28 | })
29 |
30 | tree = renderer.create(
31 | {}}
34 | user={{
35 | _id: 1,
36 | }}
37 | />
38 | )
39 | })
40 |
41 | expect(tree.toJSON()).toMatchSnapshot()
42 | })
43 |
--------------------------------------------------------------------------------
/src/__tests__/InputToolbar.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { InputToolbar } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 |
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/src/__tests__/LoadEarlier.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { LoadEarlier } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 |
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/src/__tests__/Message.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Message } from '../GiftedChat'
6 |
7 | describe('Message component', () => {
8 | it('should render and compare with snapshot', () => {
9 | const tree = renderer
10 | .create(
11 |
22 | )
23 | .toJSON()
24 |
25 | expect(tree).toMatchSnapshot()
26 | })
27 |
28 | it('should NOT render ', () => {
29 | const tree = renderer
30 | .create()
31 | .toJSON()
32 |
33 | expect(tree).toMatchSnapshot()
34 | })
35 |
36 | it('should render with Avatar', () => {
37 | const tree = renderer
38 | .create(
39 |
51 | )
52 | .toJSON()
53 |
54 | expect(tree).toMatchSnapshot()
55 | })
56 |
57 | it('should render null if user has no Avatar', () => {
58 | const tree = renderer
59 | .create(
60 |
75 | )
76 | .toJSON()
77 |
78 | expect(tree).toMatchSnapshot()
79 | })
80 | })
81 |
--------------------------------------------------------------------------------
/src/__tests__/MessageContainer.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { MessageContainer } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 |
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/src/__tests__/MessageImage.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { MessageImage } from '../GiftedChat'
6 | import { DEFAULT_TEST_MESSAGE } from './data'
7 |
8 | describe('MessageImage', () => {
9 | it('should not render and compare with snapshot', () => {
10 | const tree = renderer.create().toJSON()
11 | expect(tree).toMatchSnapshot()
12 | })
13 |
14 | it('should render and compare with snapshot', () => {
15 | const tree = renderer
16 | .create(
17 |
23 | )
24 | .toJSON()
25 | expect(tree).toMatchSnapshot()
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/src/__tests__/MessageText.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { MessageText } from '../GiftedChat'
6 |
7 | it('should render and compare with snapshot', () => {
8 | const tree = renderer.create().toJSON()
9 |
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
--------------------------------------------------------------------------------
/src/__tests__/Send.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Send } from '../GiftedChat'
6 |
7 | describe('Send', () => {
8 | it('should not render and compare with snapshot', () => {
9 | const tree = renderer.create().toJSON()
10 | expect(tree).toMatchSnapshot()
11 | })
12 |
13 | it('should always render and compare with snapshot', () => {
14 | const tree = renderer.create().toJSON()
15 | expect(tree).toMatchSnapshot()
16 | })
17 |
18 | it('should render where there is input and compare with snapshot', () => {
19 | const tree = renderer.create().toJSON()
20 | expect(tree).toMatchSnapshot()
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/__tests__/SystemMessage.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { SystemMessage } from '../GiftedChat'
6 | import { DEFAULT_TEST_MESSAGE } from './data'
7 |
8 | describe('SystemMessage', () => {
9 | it('should not render and compare with snapshot', () => {
10 | const tree = renderer.create().toJSON()
11 | expect(tree).toMatchSnapshot()
12 | })
13 |
14 | it('should render and compare with snapshot', () => {
15 | const tree = renderer
16 | .create(
17 |
23 | )
24 | .toJSON()
25 | expect(tree).toMatchSnapshot()
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/src/__tests__/Time.test.tsx:
--------------------------------------------------------------------------------
1 | import 'react-native'
2 | import React from 'react'
3 | import renderer from 'react-test-renderer'
4 |
5 | import { Time } from '../GiftedChat'
6 | import { DEFAULT_TEST_MESSAGE } from './data'
7 |
8 | describe('Time', () => {
9 | it('should not render and compare with snapshot', () => {
10 | const component = renderer.create()
11 | const tree = component.toJSON()
12 |
13 | expect(tree).toMatchSnapshot()
14 | })
15 |
16 | it('should render and compare with snapshot', () => {
17 | const component = renderer.create(
18 |
24 | )
25 | const tree = component.toJSON()
26 |
27 | expect(tree).toMatchSnapshot()
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Actions.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
42 |
61 |
76 | +
77 |
78 |
79 |
80 | `;
81 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Avatar.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
15 | renderAvatar
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Bubble.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
17 |
33 |
54 |
55 |
63 |
80 |
100 | test
101 |
102 |
103 |
104 |
105 |
116 |
128 |
140 | 7:20 PM
141 |
142 |
143 |
144 |
145 |
146 |
147 | `;
148 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Color.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should compare Color with snapshot 1`] = `
4 | {
5 | "alizarin": "#e74c3c",
6 | "backgroundTransparent": "transparent",
7 | "black": "#000",
8 | "carrot": "#e67e22",
9 | "defaultBlue": "#0084ff",
10 | "defaultColor": "#b2b2b2",
11 | "emerald": "#2ecc71",
12 | "leftBubbleBackground": "#f0f0f0",
13 | "midnightBlue": "#2c3e50",
14 | "optionTintColor": "#007AFF",
15 | "peterRiver": "#3498db",
16 | "timeTextColor": "#aaa",
17 | "turquoise": "#1abc9c",
18 | "white": "#fff",
19 | "wisteria": "#8e44ad",
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Composer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
37 | `;
38 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Constant.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should compare Constant with snapshot 1`] = `
4 | {
5 | "DATE_FORMAT": "D MMMM",
6 | "DEFAULT_PLACEHOLDER": "Type a message...",
7 | "MAX_COMPOSER_HEIGHT": 200,
8 | "MIN_COMPOSER_HEIGHT": 33,
9 | "TEST_ID": {
10 | "LOADING_WRAPPER": "GC_LOADING_CONTAINER",
11 | "SEND_TOUCHABLE": "GC_SEND_TOUCHABLE",
12 | "WRAPPER": "GC_WRAPPER",
13 | },
14 | "TIME_FORMAT": "LT",
15 | }
16 | `;
17 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Day.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Day should not render and compare with snapshot 1`] = `null`;
4 |
5 | exports[`Day should render and compare with snapshot 1`] = `null`;
6 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/GiftedAvatar.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
24 | `;
25 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/GiftedChat.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
5 |
12 |
26 |
27 |
28 | `;
29 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/InputToolbar.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
16 |
27 |
60 |
61 |
62 | `;
63 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/LoadEarlier.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
42 |
60 |
72 | Load earlier messages
73 |
74 |
75 |
76 | `;
77 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/MessageContainer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
17 |
23 |
26 |
27 | }
28 | automaticallyAdjustContentInsets={false}
29 | collapsable={false}
30 | data={[]}
31 | extraData={null}
32 | getItem={[Function]}
33 | getItemCount={[Function]}
34 | invertStickyHeaders={true}
35 | inverted={true}
36 | isInvertedVirtualizedList={true}
37 | jestAnimatedStyle={
38 | {
39 | "value": {},
40 | }
41 | }
42 | jestInlineStyle={
43 | {
44 | "flex": 1,
45 | }
46 | }
47 | keyExtractor={[Function]}
48 | onContentSizeChange={[Function]}
49 | onEndReached={[Function]}
50 | onEndReachedThreshold={0.1}
51 | onLayout={[Function]}
52 | onMomentumScrollBegin={[Function]}
53 | onMomentumScrollEnd={[Function]}
54 | onScroll={[Function]}
55 | onScrollBeginDrag={[Function]}
56 | onScrollEndDrag={[Function]}
57 | removeClippedSubviews={false}
58 | renderItem={[Function]}
59 | scrollEventThrottle={1}
60 | stickyHeaderIndices={[]}
61 | style={
62 | [
63 | {
64 | "transform": [
65 | {
66 | "scaleY": -1,
67 | },
68 | ],
69 | },
70 | {
71 | "flex": 1,
72 | },
73 | ]
74 | }
75 | viewabilityConfigCallbackPairs={[]}
76 | >
77 |
78 |
91 |
98 |
99 |
100 |
101 | `;
102 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/MessageImage.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`MessageImage should render and compare with snapshot 1`] = `
4 |
5 |
19 |
38 |
39 |
40 | `;
41 |
42 | exports[`MessageImage should not render and compare with snapshot 1`] = `null`;
43 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/MessageText.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render and compare with snapshot 1`] = `
4 |
12 |
29 |
30 | `;
31 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Send.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Send should always render and compare with snapshot 1`] = `
4 |
43 |
44 |
60 | Send
61 |
62 |
63 |
64 | `;
65 |
66 | exports[`Send should not render and compare with snapshot 1`] = `null`;
67 |
68 | exports[`Send should render where there is input and compare with snapshot 1`] = `
69 |
108 |
109 |
125 | Send
126 |
127 |
128 |
129 | `;
130 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/SystemMessage.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SystemMessage should not render and compare with snapshot 1`] = `null`;
4 |
5 | exports[`SystemMessage should render and compare with snapshot 1`] = `
6 |
24 |
25 |
38 | test
39 |
40 |
41 |
42 | `;
43 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/Time.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Time should not render and compare with snapshot 1`] = `null`;
4 |
5 | exports[`Time should render and compare with snapshot 1`] = `
6 |
18 |
30 | 10:05 AM
31 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/src/__tests__/data.ts:
--------------------------------------------------------------------------------
1 | import { IMessage } from '../types'
2 |
3 | export const DEFAULT_TEST_MESSAGE: IMessage = {
4 | _id: 'test',
5 | text: 'test',
6 | user: { _id: 'test' },
7 | createdAt: new Date(2022, 3, 17),
8 | }
9 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { isSameDay, isSameUser } from '../utils'
2 |
3 | it('should test if same day', () => {
4 | const now = new Date()
5 | expect(
6 | isSameDay(
7 | {
8 | _id: 1,
9 | text: 'test',
10 | createdAt: now,
11 | user: { _id: 1 },
12 | },
13 | {
14 | _id: 2,
15 | text: 'test2',
16 | createdAt: now,
17 | user: { _id: 2 },
18 | }
19 | )
20 | ).toBe(true)
21 | })
22 |
23 | it('should test if same user', () => {
24 | const message = {
25 | _id: 1,
26 | text: 'test',
27 | createdAt: new Date(),
28 | user: { _id: 1 },
29 | }
30 | expect(isSameUser(message, message)).toBe(true)
31 | })
32 |
--------------------------------------------------------------------------------
/src/hooks/useUpdateLayoutEffect.ts:
--------------------------------------------------------------------------------
1 | import { DependencyList, useLayoutEffect, useRef } from 'react'
2 |
3 | /**
4 | * A custom useEffect hook that only triggers on updates, not on initial mount
5 | * Idea stolen from: https://stackoverflow.com/a/55075818/1526448
6 | * @param {()=>void} effect the function to call
7 | * @param {DependencyList} dependencies the state(s) that fires the update
8 | */
9 | export function useUpdateLayoutEffect (
10 | effect: () => void,
11 | dependencies: DependencyList = []
12 | ) {
13 | const isInitialMount = useRef(true)
14 |
15 | useLayoutEffect(() => {
16 | if (isInitialMount.current)
17 | isInitialMount.current = false
18 | else
19 | effect()
20 | }, dependencies)
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './GiftedChat'
2 | export * from './Constant'
3 | export * from './utils'
4 | export * from './GiftedChatContext'
5 |
--------------------------------------------------------------------------------
/src/logging.ts:
--------------------------------------------------------------------------------
1 | const styleString = (color: string) => `color: ${color}; font-weight: bold`
2 | const headerLog = '%c[react-native-gifted-chat]'
3 |
4 | export const warning = (...args: unknown[]) =>
5 | console.log(headerLog, styleString('orange'), ...args)
6 |
7 | export const error = (...args: unknown[]) =>
8 | console.log(headerLog, styleString('red'), ...args)
9 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native'
2 |
3 | export default StyleSheet.create({
4 | fill: {
5 | flex: 1,
6 | },
7 | centerItems: {
8 | justifyContent: 'center',
9 | alignItems: 'center',
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { StyleProp, ViewStyle } from 'react-native'
2 | import { LightboxProps } from 'react-native-lightbox-v2'
3 |
4 | export { ActionsProps } from './Actions'
5 | export { AvatarProps } from './Avatar'
6 | export {
7 | BubbleProps,
8 | RenderMessageImageProps,
9 | RenderMessageVideoProps,
10 | RenderMessageAudioProps,
11 | RenderMessageTextProps
12 | } from './Bubble'
13 | export { ComposerProps } from './Composer'
14 | export { DayProps } from './Day'
15 | export { GiftedAvatarProps } from './GiftedAvatar'
16 | export { InputToolbarProps } from './InputToolbar'
17 | export { LoadEarlierProps } from './LoadEarlier'
18 | export { MessageProps } from './Message'
19 | export { MessageContainerProps } from './MessageContainer'
20 | export { MessageImageProps } from './MessageImage'
21 | export { MessageTextProps } from './MessageText'
22 | export { QuickRepliesProps } from './QuickReplies'
23 | export { SendProps } from './Send'
24 | export { SystemMessageProps } from './SystemMessage'
25 | export { TimeProps } from './Time'
26 |
27 | export type Omit = Pick>
28 |
29 | export interface LeftRightStyle {
30 | left?: StyleProp
31 | right?: StyleProp
32 | }
33 |
34 | type renderFunction = (x: unknown) => React.ReactNode
35 |
36 | export interface User {
37 | _id: string | number
38 | name?: string
39 | avatar?: string | number | renderFunction
40 | }
41 |
42 | export interface Reply {
43 | title: string
44 | value: string
45 | messageId?: number | string
46 | }
47 |
48 | export interface QuickReplies {
49 | type: 'radio' | 'checkbox'
50 | values: Reply[]
51 | keepIt?: boolean
52 | }
53 |
54 | export interface IMessage {
55 | _id: string | number
56 | text: string
57 | createdAt: Date | number
58 | user: User
59 | image?: string
60 | video?: string
61 | audio?: string
62 | system?: boolean
63 | sent?: boolean
64 | received?: boolean
65 | pending?: boolean
66 | quickReplies?: QuickReplies
67 | }
68 |
69 | export type IChatMessage = IMessage
70 |
71 | export interface MessageVideoProps {
72 | currentMessage: TMessage
73 | containerStyle?: StyleProp
74 | videoStyle?: StyleProp
75 | videoProps?: object
76 | lightboxProps?: LightboxProps
77 | }
78 |
79 | export interface MessageAudioProps {
80 | currentMessage: TMessage
81 | containerStyle?: StyleProp
82 | audioStyle?: StyleProp
83 | audioProps?: object
84 | }
85 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | import { IMessage } from './types'
4 |
5 | export function isSameDay (
6 | currentMessage: IMessage,
7 | diffMessage: IMessage | null | undefined
8 | ) {
9 | if (!diffMessage || !diffMessage.createdAt)
10 | return false
11 |
12 | const currentCreatedAt = dayjs(currentMessage.createdAt)
13 | const diffCreatedAt = dayjs(diffMessage.createdAt)
14 |
15 | if (!currentCreatedAt.isValid() || !diffCreatedAt.isValid())
16 | return false
17 |
18 | return currentCreatedAt.isSame(diffCreatedAt, 'day')
19 | }
20 |
21 | export function isSameUser (
22 | currentMessage: IMessage,
23 | diffMessage: IMessage | null | undefined
24 | ) {
25 | return !!(
26 | diffMessage &&
27 | diffMessage.user &&
28 | currentMessage.user &&
29 | diffMessage.user._id === currentMessage.user._id
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | require('react-native-reanimated').setUpTests()
2 |
3 | // mocks
4 | jest.mock('react-native-lightbox-v2', () => 'Lightbox')
5 | jest.mock('react-native-keyboard-controller', () =>
6 | require('react-native-keyboard-controller/jest'),
7 | )
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "outDir": "./lib",
5 | "strict": true,
6 | "jsx": "react-native",
7 | "target": "ES2020",
8 | "module": "ESNext",
9 | "moduleResolution": "bundler",
10 | "allowSyntheticDefaultImports": true,
11 | "noImplicitAny": true,
12 | "experimentalDecorators": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "allowJs": true,
16 | "checkJs": true,
17 | "strictNullChecks": true,
18 | "skipDefaultLibCheck": true,
19 | "skipLibCheck": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noImplicitReturns": true,
23 | "noImplicitThis": true,
24 | "importHelpers": false,
25 | "alwaysStrict": true,
26 | "forceConsistentCasingInFileNames": true,
27 | "strictFunctionTypes": true,
28 | "resolveJsonModule": true,
29 | "noFallthroughCasesInSwitch": true,
30 | "strictPropertyInitialization": false,
31 | "lib": ["ES2020"],
32 | "typeRoots": ["./node_modules/@types", "./@types"]
33 | },
34 | "include": ["src", "./types.d.ts"],
35 | "exclude": ["node_modules", "src/__tests__", "example/node_modules", "lib"]
36 | }
37 |
--------------------------------------------------------------------------------