├── .eslintignore ├── example ├── .env ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── App.js │ └── serviceWorker.js ├── config │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── .gitignore ├── scripts │ ├── test.js │ ├── start.js │ └── build.js ├── README.md └── package.json ├── src ├── index.js ├── Constant.js ├── Color.js ├── GiftedChatInteractionManager.js ├── utils.js ├── SystemMessage.js ├── MessageVideo.js ├── WebScrollView.js ├── Send.js ├── TouchableOpacity.js ├── Day.js ├── Time.js ├── ParsedText.js ├── Actions.js ├── LoadEarlier.js ├── TextExtraction.js ├── MessageImage.js ├── Composer.js ├── InputToolbar.js ├── Avatar.js ├── GiftedAvatar.js ├── Message.js ├── MessageText.js ├── MessageContainer.js ├── Bubble.js └── GiftedChat.js ├── .babelrc ├── .npmignore ├── .watchmanconfig ├── .editorconfig ├── screenshots ├── gifted-chat-1.png └── gifted-chat-2.png ├── .gitignore ├── ISSUE_TEMPLATE.md ├── rollup.config.js ├── LICENSE ├── .eslintrc ├── .github └── stale.yml ├── package.json ├── README.md └── index.d.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './GiftedChat'; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | TODO.md 3 | screenshots/ 4 | 5 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [".git", "node_modules"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johniak/react-web-gifted-chat/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/gifted-chat-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johniak/react-web-gifted-chat/HEAD/screenshots/gifted-chat-1.png -------------------------------------------------------------------------------- /screenshots/gifted-chat-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johniak/react-web-gifted-chat/HEAD/screenshots/gifted-chat-2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .expo/ 4 | npm-debug.log 5 | TODO.md 6 | dist/ 7 | .idea 8 | .vscode 9 | Exponent-*.app 10 | *.log 11 | -------------------------------------------------------------------------------- /src/Constant.js: -------------------------------------------------------------------------------- 1 | export const MIN_COMPOSER_HEIGHT = 41; 2 | export const MAX_COMPOSER_HEIGHT = 41; 3 | export const DEFAULT_PLACEHOLDER = 'Type a message...'; 4 | export const DATE_FORMAT = 'll'; 5 | export const TIME_FORMAT = 'LT'; 6 | -------------------------------------------------------------------------------- /example/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /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-web-gifted-chat version: [FILL THIS OUT] 18 | * Browser(s) (Chrome, etc.): [FILL THIS OUT] 19 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/Color.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/GiftedChatInteractionManager.js: -------------------------------------------------------------------------------- 1 | import { InteractionManager } from 'react-native'; 2 | 3 | export default { 4 | ...InteractionManager, 5 | runAfterInteractions: (f) => { 6 | // ensure f get called, timeout at 500ms 7 | // @gre workaround https://github.com/facebook/react-native/issues/8624 8 | let called = false; 9 | const timeout = setTimeout(() => { 10 | called = true; 11 | f(); 12 | }, 500); 13 | InteractionManager.runAfterInteractions(() => { 14 | if (called) return; 15 | clearTimeout(timeout); 16 | f(); 17 | }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export function isSameDay(currentMessage = {}, diffMessage = {}) { 4 | if (!diffMessage.createdAt) { 5 | return false; 6 | } 7 | 8 | const currentCreatedAt = moment(currentMessage.createdAt); 9 | const diffCreatedAt = moment(diffMessage.createdAt); 10 | 11 | if (!currentCreatedAt.isValid() || !diffCreatedAt.isValid()) { 12 | return false; 13 | } 14 | 15 | return currentCreatedAt.isSame(diffCreatedAt, 'day'); 16 | } 17 | 18 | export function isSameUser(currentMessage = {}, diffMessage = {}) { 19 | return !!(diffMessage.user && currentMessage.user && diffMessage.user.id === currentMessage.user.id); 20 | } 21 | -------------------------------------------------------------------------------- /example/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `module.exports = { 14 | __esModule: true, 15 | default: ${assetFilename}, 16 | ReactComponent: (props) => ({ 17 | $$typeof: Symbol.for('react.element'), 18 | type: 'svg', 19 | ref: null, 20 | key: null, 21 | props: Object.assign({}, props, { 22 | children: ${assetFilename} 23 | }) 24 | }), 25 | };`; 26 | } 27 | 28 | return `module.exports = ${assetFilename};`; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import postcss from 'rollup-plugin-postcss'; 5 | import fs from 'fs'; 6 | 7 | 8 | const pkg = JSON.parse(fs.readFileSync('./package.json')); 9 | 10 | 11 | const external = Object.keys(pkg.dependencies || {}); 12 | export default { 13 | input: 'src/index.js', 14 | output: { 15 | file: 'dist/bundle.js', 16 | format: 'cjs', 17 | }, 18 | // All the used libs needs to be here 19 | external: [ 20 | ...external, 21 | 'prop-types', 22 | 'react-native', 23 | 'moment/min/moment-with-locales.min', 24 | ], 25 | plugins: [ 26 | resolve(), 27 | commonjs({ 28 | // non-CommonJS modules will be ignored, but you can also 29 | // specifically include/exclude files 30 | include: 'node_modules/**', 31 | }), 32 | babel({ 33 | runtimeHelpers: true, 34 | exclude: 'node_modules/**', 35 | }), 36 | postcss({ 37 | plugins: [], 38 | }), 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /src/SystemMessage.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }] */ 2 | 3 | import React from 'react'; 4 | import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; 5 | import PropTypes from 'prop-types'; 6 | import Color from './Color'; 7 | 8 | export default function SystemMessage({ currentMessage, containerStyle, wrapperStyle, textStyle }) { 9 | return ( 10 | 11 | 12 | {currentMessage.text} 13 | 14 | 15 | ); 16 | } 17 | 18 | const styles = StyleSheet.create({ 19 | container: { 20 | alignItems: 'center', 21 | justifyContent: 'center', 22 | flex: 1, 23 | marginTop: 5, 24 | marginBottom: 10, 25 | }, 26 | text: { 27 | backgroundColor: Color.backgroundTransparent, 28 | color: Color.defaultColor, 29 | fontSize: 12, 30 | fontWeight: '300', 31 | }, 32 | }); 33 | 34 | SystemMessage.defaultProps = { 35 | currentMessage: { 36 | system: false, 37 | }, 38 | containerStyle: {}, 39 | wrapperStyle: {}, 40 | textStyle: {}, 41 | }; 42 | 43 | SystemMessage.propTypes = { 44 | currentMessage: PropTypes.object, 45 | containerStyle: ViewPropTypes.style, 46 | wrapperStyle: ViewPropTypes.style, 47 | textStyle: Text.propTypes.style, 48 | }; 49 | -------------------------------------------------------------------------------- /src/MessageVideo.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import { StyleSheet, View, ViewPropTypes } from 'react-native'; 4 | //import Video from 'react-native-video'; 5 | 6 | 7 | export default function MessageVideo({ 8 | containerStyle, 9 | videoProps, 10 | videoStyle, 11 | currentMessage, 12 | }) { 13 | return ( 14 | // eslint-disable-next-line no-use-before-define 15 | 16 | {/* 17 | 28 | ); 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | container: { 33 | }, 34 | }); 35 | 36 | MessageVideo.defaultProps = { 37 | currentMessage: { 38 | // video: null, 39 | }, 40 | containerStyle: {}, 41 | videoStyle: { 42 | width: 150, 43 | height: 100, 44 | borderRadius: 13, 45 | margin: 3, 46 | }, 47 | videoProps: {}, 48 | }; 49 | 50 | MessageVideo.propTypes = { 51 | currentMessage: PropTypes.object, 52 | containerStyle: ViewPropTypes.style, 53 | videoStyle: ViewPropTypes.style, 54 | videoProps: PropTypes.object, 55 | }; 56 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es6": true, 6 | "mocha": true, 7 | "jest": true 8 | }, 9 | "parser": "babel-eslint", 10 | "extends": "airbnb", 11 | "plugins": [ 12 | "mocha" 13 | ], 14 | "globals": { 15 | "__DEV__": false, 16 | "babelHelpers": false 17 | }, 18 | "rules": { 19 | "no-underscore-dangle": 0, 20 | "no-return-assign": 0, 21 | "max-len": 0, 22 | "no-shadow": 0, 23 | "react/jsx-filename-extension": 0, 24 | "react/forbid-prop-types": 0, 25 | "no-use-before-define": 0, 26 | "no-unused-vars": 1, 27 | "eqeqeq": 1, 28 | "global-require": 0, 29 | "class-methods-use-this": 0, 30 | "react/sort-comp": 0, 31 | "react/require-default-props": 0, 32 | "react/jsx-tag-spacing": { 33 | "closingSlash": "never", 34 | "beforeSelfClosing": "always", 35 | "afterOpening": "never", 36 | "beforeClosing": "allow" 37 | }, 38 | "radix": 0, 39 | "react/prefer-stateless-function": 0, 40 | "no-plusplus": 0, 41 | "no-console": 0, 42 | "no-restricted-syntax": 0, 43 | "no-continue": 0, 44 | "guard-for-in": 0, 45 | "no-debugger": 0, 46 | "prefer-destructuring": 0, 47 | "react/destructuring-assignment": 1, 48 | "object-curly-newline": ["warn", { 49 | "consistent": true, 50 | "multiline": true 51 | }] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--coverage') === -1 && 45 | argv.indexOf('--watchAll') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Web GiftedChat 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/WebScrollView.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | import { FlatList, View, StyleSheet, Keyboard, TouchableOpacity, Text } from 'react-native'; 5 | 6 | export default class WebScrollView extends Component { 7 | renderItem =(item, index) => { 8 | const { renderItem } = this.props; 9 | return renderItem({ item, index }); 10 | } 11 | 12 | render() { 13 | const { ListHeaderComponent, ListFooterComponent, data, inverted } = this.props; 14 | let messages = data; 15 | if (!inverted) { 16 | messages = data.slice().reverse(); 17 | } 18 | return ( 19 |
20 | {ListHeaderComponent()} 21 | {messages.map(this.renderItem)} 22 | {ListFooterComponent()} 23 |
24 | ); 25 | } 26 | } 27 | 28 | const styles = { 29 | container: { 30 | height: '100%', 31 | minHeight: '100%', 32 | width: '100%', 33 | overflow: 'auto', 34 | display: 'flex', 35 | flexDirection: 'column-reverse', 36 | flex: 1, 37 | alignItems: 'stretch', 38 | }, 39 | }; 40 | 41 | WebScrollView.defaultProps = { 42 | data: [], 43 | extraData: {}, 44 | ListHeaderComponent: () => {}, 45 | ListFooterComponent: () => {}, 46 | inverted: false, 47 | }; 48 | 49 | WebScrollView.propTypes = { 50 | data: PropTypes.arrayOf(PropTypes.object), 51 | extraData: PropTypes.object, 52 | inverted: PropTypes.bool, 53 | renderFooter: PropTypes.func, 54 | keyExtractor: PropTypes.func, 55 | enableEmptySections: PropTypes.bool, 56 | automaticallyAdjustContentInsets: PropTypes.bool, 57 | contentContainerStyle: PropTypes.object, 58 | renderItem: PropTypes.func, 59 | ListHeaderComponent: PropTypes.func, 60 | ListFooterComponent: PropTypes.func, 61 | }; 62 | -------------------------------------------------------------------------------- /src/Send.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }] */ 2 | 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; 6 | import Color from './Color'; 7 | import TouchableOpacity from './TouchableOpacity'; 8 | 9 | export default function Send({ text, containerStyle, onSend, children, textStyle, label, alwaysShowSend }) { 10 | if (alwaysShowSend || text.trim().length > 0) { 11 | return ( 12 | { 18 | console.log('asdasdasd') 19 | onSend({ text: text.trim() }, true); 20 | }} 21 | accessibilityTraits="button" 22 | > 23 | {children || {label}} 24 | 25 | ); 26 | } 27 | return ; 28 | } 29 | 30 | const styles = StyleSheet.create({ 31 | container: { 32 | height: 44, 33 | justifyContent: 'flex-end', 34 | }, 35 | text: { 36 | color: Color.defaultBlue, 37 | fontWeight: '600', 38 | fontSize: 17, 39 | backgroundColor: Color.backgroundTransparent, 40 | marginBottom: 12, 41 | marginLeft: 10, 42 | marginRight: 10, 43 | }, 44 | }); 45 | 46 | Send.defaultProps = { 47 | text: '', 48 | onSend: () => {}, 49 | label: 'Send', 50 | containerStyle: {}, 51 | textStyle: {}, 52 | children: null, 53 | alwaysShowSend: false, 54 | }; 55 | 56 | Send.propTypes = { 57 | text: PropTypes.string, 58 | onSend: PropTypes.func, 59 | label: PropTypes.string, 60 | containerStyle: ViewPropTypes.style, 61 | textStyle: Text.propTypes.style, 62 | children: PropTypes.element, 63 | alwaysShowSend: PropTypes.bool, 64 | }; 65 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - pinned 13 | - security 14 | - "[Status] Maybe Later" 15 | 16 | # Set to true to ignore issues in a project (defaults to false) 17 | exemptProjects: false 18 | 19 | # Set to true to ignore issues in a milestone (defaults to false) 20 | exemptMilestones: false 21 | 22 | # Label to use when marking as stale 23 | staleLabel: wontfix 24 | 25 | # Comment to post when marking as stale. Set to `false` to disable 26 | markComment: > 27 | This issue has been automatically marked as stale because it has not had 28 | recent activity. It will be closed if no further activity occurs. Thank you 29 | for your contributions. 30 | 31 | # Comment to post when removing the stale label. 32 | # unmarkComment: > 33 | # Your comment here. 34 | 35 | # Comment to post when closing a stale Issue or Pull Request. 36 | # closeComment: > 37 | # Your comment here. 38 | 39 | # Limit the number of actions per hour, from 1-30. Default is 30 40 | limitPerRun: 30 41 | 42 | # Limit to only `issues` or `pulls` 43 | # only: issues 44 | 45 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 46 | # pulls: 47 | # daysUntilStale: 30 48 | # markComment: > 49 | # This pull request has been automatically marked as stale because it has not had 50 | # recent activity. It will be closed if no further activity occurs. Thank you 51 | # for your contributions. 52 | 53 | # issues: 54 | # exemptLabels: 55 | # - confirmed -------------------------------------------------------------------------------- /src/TouchableOpacity.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 3 | import PropTypes from 'prop-types'; 4 | import React, { Component } from 'react'; 5 | 6 | 7 | export default class TouchableOpacity extends Component { 8 | state={ 9 | pressed: false, 10 | } 11 | 12 | handleButtonPress= () => { 13 | const { onLongPress, withoutFeedback } = this.props; 14 | this.setState({ pressed: true }); 15 | this.buttonPressTimer = setTimeout(onLongPress, 500); 16 | } 17 | 18 | handleButtonRelease= () => { 19 | this.setState({ pressed: false }); 20 | clearTimeout(this.buttonPressTimer); 21 | } 22 | 23 | 24 | render() { 25 | const { children, onPress, withoutFeedback } = this.props; 26 | const { pressed } = this.state; 27 | let { style } = this.props; 28 | if (!withoutFeedback) { 29 | style = { ...styles.container, ...(pressed ? styles.containerPressed : styles.containerNotPressed), ...style }; 30 | } 31 | return ( 32 |
41 | {children} 42 |
43 | ); 44 | } 45 | } 46 | 47 | const styles = { 48 | container: { 49 | cursor: 'pointer', 50 | }, 51 | containerNotPressed: { 52 | opacity: 1, 53 | }, 54 | containerPressed: { 55 | opacity: 0.5, 56 | }, 57 | }; 58 | 59 | TouchableOpacity.defaultProps = { 60 | onPress: () => {}, 61 | onLongPress: () => {}, 62 | withoutFeedback: false, 63 | style: {}, 64 | }; 65 | 66 | TouchableOpacity.propTypes = { 67 | onPress: PropTypes.func, 68 | onLongPress: PropTypes.func, 69 | children: PropTypes.node, 70 | withoutFeedback: PropTypes.bool, 71 | style: PropTypes.object, 72 | }; 73 | -------------------------------------------------------------------------------- /src/Day.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }] */ 2 | 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; 6 | import moment from 'moment'; 7 | 8 | import Color from './Color'; 9 | 10 | import { isSameDay } from './utils'; 11 | import { DATE_FORMAT } from './Constant'; 12 | 13 | export default function Day( 14 | { dateFormat, currentMessage, previousMessage, nextMessage, containerStyle, wrapperStyle, textStyle, inverted }, 15 | context, 16 | ) { 17 | if (!isSameDay(currentMessage, inverted ? previousMessage : nextMessage)) { 18 | return ( 19 | 20 | 21 | 22 | {moment(currentMessage.createdAt) 23 | .locale(context.getLocale()) 24 | .format(dateFormat) 25 | .toUpperCase()} 26 | 27 | 28 | 29 | ); 30 | } 31 | return null; 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | container: { 36 | alignItems: 'center', 37 | justifyContent: 'center', 38 | marginTop: 5, 39 | marginBottom: 10, 40 | }, 41 | text: { 42 | backgroundColor: Color.backgroundTransparent, 43 | color: Color.defaultColor, 44 | fontSize: 12, 45 | fontWeight: '600', 46 | }, 47 | }); 48 | 49 | Day.contextTypes = { 50 | getLocale: PropTypes.func, 51 | }; 52 | 53 | Day.defaultProps = { 54 | currentMessage: { 55 | // TODO: test if crash when createdAt === null 56 | createdAt: null, 57 | }, 58 | previousMessage: {}, 59 | nextMessage: {}, 60 | containerStyle: {}, 61 | wrapperStyle: {}, 62 | textStyle: {}, 63 | dateFormat: DATE_FORMAT, 64 | }; 65 | 66 | Day.propTypes = { 67 | currentMessage: PropTypes.object, 68 | previousMessage: PropTypes.object, 69 | nextMessage: PropTypes.object, 70 | inverted: PropTypes.bool, 71 | containerStyle: ViewPropTypes.style, 72 | wrapperStyle: ViewPropTypes.style, 73 | textStyle: Text.propTypes.style, 74 | dateFormat: PropTypes.string, 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-web-gifted-chat", 3 | "version": "0.6.7", 4 | "description": "The most complete chat UI for React Web", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/johniak/react-web-gifted-chat.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "react-component", 13 | "messenger", 14 | "message", 15 | "chat" 16 | ], 17 | "author": "Jan Romaniak", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/johniak/react-web-gifted-chat/issues" 21 | }, 22 | "homepage": "https://github.com/johniak/react-web-gifted-chat#readme", 23 | "scripts": { 24 | "lint": "eslint . --ext .js,.jsx", 25 | "compile": "export BABEL_ENV='production' && rm -rf ./dist && mkdir dist && rollup -c -i src/index.js -o dist/index.js" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.1.5", 29 | "@babel/core": "7.1.0", 30 | "@babel/runtime": "7.1.5", 31 | "babel-core": "7.0.0-bridge.0", 32 | "babel-eslint": "9.0.0", 33 | "babel-jest": "23.6.0", 34 | "babel-loader": "8.0.4", 35 | "babel-plugin-named-asset-import": "^0.2.3", 36 | "babel-preset-react-app": "^6.1.0", 37 | "eslint": "5.9.0", 38 | "eslint-config-airbnb": "17.1.0", 39 | "eslint-config-cooperka": "0.3.1", 40 | "eslint-plugin-import": "2.14.0", 41 | "eslint-plugin-jsx-a11y": "6.1.2", 42 | "eslint-plugin-mocha": "^5.2.1", 43 | "eslint-plugin-react": "7.11.1", 44 | "eslint-plugin-react-native": "3.5.0", 45 | "rollup": "^0.67.4", 46 | "rollup-plugin-babel": "^4.0.3", 47 | "rollup-plugin-commonjs": "^9.2.0", 48 | "rollup-plugin-node-resolve": "^3.4.0", 49 | "rollup-plugin-postcss": "^1.6.3" 50 | }, 51 | "dependencies": { 52 | "md5": "2.2.1", 53 | "moment": "2.22.2", 54 | "prop-types": "^15.6.2", 55 | "react": "^16.6.3", 56 | "react-app-polyfill": "^0.1.3", 57 | "react-art": "^16.6.3", 58 | "react-dev-utils": "^6.1.1", 59 | "react-dom": "^16.6.3", 60 | "react-image-lightbox": "^5.1.0", 61 | "react-native-web": "0.9.9", 62 | "shallowequal": "1.1.0", 63 | "uuid": "3.3.2" 64 | }, 65 | "peerDependencies": {} 66 | } 67 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /src/Time.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }] */ 2 | 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; 6 | 7 | import moment from 'moment'; 8 | 9 | import Color from './Color'; 10 | import { TIME_FORMAT } from './Constant'; 11 | 12 | export default function Time( 13 | { position, containerStyle, currentMessage, timeFormat, textStyle, timeTextStyle }, 14 | context, 15 | ) { 16 | return ( 17 | 18 | 19 | {moment(currentMessage.createdAt) 20 | .locale(context.getLocale()) 21 | .format(timeFormat)} 22 | 23 | 24 | ); 25 | } 26 | 27 | const containerStyle = { 28 | marginLeft: 10, 29 | marginRight: 10, 30 | marginBottom: 5, 31 | }; 32 | 33 | const textStyle = { 34 | fontSize: 10, 35 | backgroundColor: 'transparent', 36 | textAlign: 'right', 37 | }; 38 | 39 | const styles = { 40 | left: StyleSheet.create({ 41 | container: { 42 | ...containerStyle, 43 | }, 44 | text: { 45 | color: Color.timeTextColor, 46 | ...textStyle, 47 | }, 48 | }), 49 | right: StyleSheet.create({ 50 | container: { 51 | ...containerStyle, 52 | }, 53 | text: { 54 | color: Color.white, 55 | ...textStyle, 56 | }, 57 | }), 58 | }; 59 | 60 | Time.contextTypes = { 61 | getLocale: PropTypes.func, 62 | }; 63 | 64 | Time.defaultProps = { 65 | position: 'left', 66 | currentMessage: { 67 | createdAt: null, 68 | }, 69 | containerStyle: {}, 70 | textStyle: {}, 71 | timeFormat: TIME_FORMAT, 72 | timeTextStyle: {}, 73 | }; 74 | 75 | Time.propTypes = { 76 | position: PropTypes.oneOf(['left', 'right']), 77 | currentMessage: PropTypes.object, 78 | containerStyle: PropTypes.shape({ 79 | left: ViewPropTypes.style, 80 | right: ViewPropTypes.style, 81 | }), 82 | textStyle: PropTypes.shape({ 83 | left: Text.propTypes.style, 84 | right: Text.propTypes.style, 85 | }), 86 | timeFormat: PropTypes.string, 87 | timeTextStyle: PropTypes.shape({ 88 | left: Text.propTypes.style, 89 | right: Text.propTypes.style, 90 | }), 91 | }; 92 | -------------------------------------------------------------------------------- /src/ParsedText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactNative from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import TextExtraction from './TextExtraction'; 6 | 7 | const PATTERNS = { 8 | url: /(https?:\/\/|www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/i, 9 | phone: /[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}/, 10 | email: /\S+@\S+\.\S+/, 11 | }; 12 | 13 | const defaultParseShape = PropTypes.shape({ 14 | ...ReactNative.Text.propTypes, 15 | type: PropTypes.oneOf(Object.keys(PATTERNS)).isRequired, 16 | }); 17 | 18 | const customParseShape = PropTypes.shape({ 19 | ...ReactNative.Text.propTypes, 20 | pattern: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(RegExp)]).isRequired, 21 | }); 22 | 23 | class ParsedText extends React.Component { 24 | static displayName = 'ParsedText'; 25 | 26 | static propTypes = { 27 | ...ReactNative.Text.propTypes, 28 | parse: PropTypes.arrayOf( 29 | PropTypes.oneOfType([defaultParseShape, customParseShape]), 30 | ), 31 | childrenProps: PropTypes.shape(ReactNative.Text.propTypes), 32 | }; 33 | 34 | static defaultProps = { 35 | parse: null, 36 | }; 37 | 38 | setNativeProps(nativeProps) { 39 | this._root.setNativeProps(nativeProps); 40 | } 41 | 42 | getPatterns() { 43 | return this.props.parse.map((option) => { 44 | const { type, ...patternOption } = option; 45 | if (type) { 46 | if (!PATTERNS[type]) { 47 | throw new Error(`${option.type} is not a supported type`); 48 | } 49 | patternOption.pattern = PATTERNS[type]; 50 | } 51 | 52 | return patternOption; 53 | }); 54 | } 55 | 56 | getParsedText() { 57 | if (!this.props.parse) { return this.props.children; } 58 | if (typeof this.props.children !== 'string') { return this.props.children; } 59 | 60 | const textExtraction = new TextExtraction(this.props.children, this.getPatterns()); 61 | const childrenProps = this.props.childrenProps || {}; 62 | const props = { ...this.props }; 63 | delete props.childrenProps; 64 | return textExtraction.parse().map((props, index) => ( 65 | 70 | )); 71 | } 72 | 73 | render() { 74 | const props = { ...this.props }; 75 | delete props.childrenProps; 76 | return ( 77 | this._root = ref} 79 | {...props} 80 | > 81 | {this.getParsedText()} 82 | 83 | ); 84 | } 85 | } 86 | 87 | export default ParsedText; 88 | -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {GiftedChat} from 'react-web-gifted-chat' 3 | 4 | const loremIpsum ='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum'; 5 | 6 | 7 | const messages = []; 8 | messages.push(generateMessage(`Idylla 2`, 3, {image:'https://www.wykop.pl/cdn/c3201142/comment_Sc8p2KAVLx3EyNIpXuOXngk3ZYJ0g8eq.jpg'})); 9 | messages.push(generateMessage(`Goood 1`, 2, {image:'http://img2.dmty.pl//uploads/201010/1286036107_by_julia2332_600.jpg'})); 10 | messages.push(generateMessage(`This is a great example of system message`, 2, {system: true})); 11 | 12 | for (let i = 0; i < 30; i++) { 13 | messages.push(generateMessage(loremIpsum.substring(0,(Math.random() * 100000)%loremIpsum.length), i)) 14 | } 15 | 16 | 17 | 18 | function generateMessage(text, index, additionalData) { 19 | return { 20 | id: Math.round(Math.random() * 1000000), 21 | text: text, 22 | createdAt: new Date(), 23 | user: { 24 | id: index % 3 === 0 ? 1 : 2, 25 | name: 'Johniak', 26 | }, 27 | ...additionalData, 28 | } 29 | } 30 | 31 | class App extends Component { 32 | constructor() { 33 | super() 34 | this.state = { 35 | messages: messages 36 | } 37 | this.onSend = this.onSend.bind(this) 38 | } 39 | 40 | renderLoading() { 41 | return (
Loading...
) 42 | } 43 | 44 | onSend(messages) { 45 | for(let message of messages){ 46 | this.setState({messages: [message,...this.state.messages]}) 47 | } 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 |
54 | Converstions 55 |
56 |
57 | 60 |
61 |
62 | Conversation details 63 |
64 |
65 | ); 66 | } 67 | } 68 | const styles = { 69 | container: { 70 | flex: 1, 71 | display: "flex", 72 | flexDirection: "row", 73 | height: "100vh", 74 | }, 75 | conversationList: { 76 | display:'flex', 77 | flex: 1, 78 | }, 79 | chat: { 80 | display: "flex", 81 | flex: 3, 82 | flexDirection: "column", 83 | borderWidth: "1px", 84 | borderColor: "#ccc", 85 | borderRightStyle: "solid", 86 | borderLeftStyle: "solid", 87 | }, 88 | converationDetails: { 89 | display:'flex', 90 | flex: 1, 91 | } 92 | } 93 | 94 | export default App; 95 | -------------------------------------------------------------------------------- /src/Actions.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }] */ 2 | 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import { StyleSheet, Text, TouchableOpacity, View, ViewPropTypes } from 'react-native'; 6 | import Color from './Color'; 7 | 8 | export default class Actions extends React.Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.onActionsPress = this.onActionsPress.bind(this); 13 | } 14 | 15 | onActionsPress() { 16 | const { options } = this.props; 17 | const optionKeys = Object.keys(options); 18 | const cancelButtonIndex = optionKeys.indexOf('Cancel'); 19 | this.context.actionSheet().showActionSheetWithOptions( 20 | { 21 | options: optionKeys, 22 | cancelButtonIndex, 23 | tintColor: this.props.optionTintColor, 24 | }, 25 | (buttonIndex) => { 26 | const key = optionKeys[buttonIndex]; 27 | if (key) { 28 | options[key](this.props); 29 | } 30 | }, 31 | ); 32 | } 33 | 34 | renderIcon() { 35 | if (this.props.icon) { 36 | return this.props.icon(); 37 | } 38 | return ( 39 | 40 | + 41 | 42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 | 51 | {this.renderIcon()} 52 | 53 | ); 54 | } 55 | 56 | } 57 | 58 | const styles = StyleSheet.create({ 59 | container: { 60 | width: 26, 61 | height: 26, 62 | marginLeft: 10, 63 | marginBottom: 10, 64 | }, 65 | wrapper: { 66 | borderRadius: 13, 67 | borderColor: Color.defaultColor, 68 | borderWidth: 2, 69 | flex: 1, 70 | }, 71 | iconText: { 72 | color: Color.defaultColor, 73 | fontWeight: 'bold', 74 | fontSize: 16, 75 | backgroundColor: Color.backgroundTransparent, 76 | textAlign: 'center', 77 | }, 78 | }); 79 | 80 | Actions.contextTypes = { 81 | actionSheet: PropTypes.func, 82 | }; 83 | 84 | Actions.defaultProps = { 85 | onSend: () => { }, 86 | options: {}, 87 | optionTintColor: Color.optionTintColor, 88 | icon: null, 89 | containerStyle: {}, 90 | iconTextStyle: {}, 91 | wrapperStyle: {}, 92 | }; 93 | 94 | Actions.propTypes = { 95 | onSend: PropTypes.func, 96 | options: PropTypes.object, 97 | optionTintColor: PropTypes.string, 98 | icon: PropTypes.func, 99 | onPressActionButton: PropTypes.func, 100 | wrapperStyle: ViewPropTypes.style, 101 | containerStyle: ViewPropTypes.style, 102 | iconTextStyle: Text.propTypes.style, 103 | }; 104 | -------------------------------------------------------------------------------- /src/LoadEarlier.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: ["error", { "variables": false }], react-native/no-inline-styles: 0 */ 2 | 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import { 6 | ActivityIndicator, 7 | StyleSheet, 8 | Text, 9 | TouchableOpacity, 10 | View, 11 | ViewPropTypes, 12 | } from 'react-native'; 13 | import Color from './Color'; 14 | 15 | export default class LoadEarlier extends React.Component { 16 | 17 | renderLoading() { 18 | if (this.props.isLoadingEarlier === false) { 19 | return ( 20 | 21 | {this.props.label} 22 | 23 | ); 24 | } 25 | return ( 26 | 27 | 28 | {this.props.label} 29 | 30 | 35 | 36 | ); 37 | } 38 | render() { 39 | return ( 40 | { 43 | if (this.props.onLoadEarlier) { 44 | this.props.onLoadEarlier(); 45 | } 46 | }} 47 | disabled={this.props.isLoadingEarlier === true} 48 | accessibilityTraits="button" 49 | > 50 | 51 | {this.renderLoading()} 52 | 53 | 54 | ); 55 | } 56 | 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | alignItems: 'center', 62 | marginTop: 5, 63 | marginBottom: 10, 64 | }, 65 | wrapper: { 66 | alignItems: 'center', 67 | justifyContent: 'center', 68 | backgroundColor: Color.defaultColor, 69 | borderRadius: 15, 70 | height: 30, 71 | paddingLeft: 10, 72 | paddingRight: 10, 73 | }, 74 | text: { 75 | backgroundColor: Color.backgroundTransparent, 76 | color: Color.white, 77 | fontSize: 12, 78 | }, 79 | activityIndicator: { 80 | marginTop: 0, 81 | }, 82 | }); 83 | 84 | LoadEarlier.defaultProps = { 85 | onLoadEarlier: () => { }, 86 | isLoadingEarlier: false, 87 | label: 'Load earlier messages', 88 | containerStyle: {}, 89 | wrapperStyle: {}, 90 | textStyle: {}, 91 | activityIndicatorStyle: {}, 92 | }; 93 | 94 | LoadEarlier.propTypes = { 95 | onLoadEarlier: PropTypes.func, 96 | isLoadingEarlier: PropTypes.bool, 97 | label: PropTypes.string, 98 | containerStyle: ViewPropTypes.style, 99 | wrapperStyle: ViewPropTypes.style, 100 | textStyle: Text.propTypes.style, 101 | activityIndicatorStyle: ViewPropTypes.style, 102 | }; 103 | -------------------------------------------------------------------------------- /src/TextExtraction.js: -------------------------------------------------------------------------------- 1 | class TextExtraction { 2 | /** 3 | * @param {String} text - Text to be parsed 4 | * @param {Object[]} patterns - Patterns to be used when parsed 5 | * other options than pattern would be added to the parsed content 6 | * @param {RegExp} patterns[].pattern - RegExp to be used for parsing 7 | */ 8 | constructor(text, patterns) { 9 | this.text = text; 10 | this.patterns = patterns || []; 11 | } 12 | 13 | /** 14 | * Returns parts of the text with their own props 15 | * @return {Object[]} - props for all the parts of the text 16 | */ 17 | parse() { 18 | let parsedTexts = [{children: this.text}]; 19 | this.patterns.forEach((pattern) => { 20 | let newParts = []; 21 | 22 | parsedTexts.forEach((parsedText) => { 23 | // Only allow for now one parsing 24 | if (parsedText._matched) { 25 | newParts.push(parsedText); 26 | 27 | return; 28 | } 29 | 30 | let parts = []; 31 | let textLeft = parsedText.children; 32 | 33 | while (textLeft) { 34 | let matches = pattern.pattern.exec(textLeft); 35 | 36 | if (!matches) { break; } 37 | 38 | let previousText = textLeft.substr(0, matches.index); 39 | 40 | parts.push({children: previousText}); 41 | 42 | parts.push(this.getMatchedPart(pattern, matches[0], matches)); 43 | 44 | textLeft = textLeft.substr(matches.index + matches[0].length); 45 | } 46 | 47 | parts.push({children: textLeft}); 48 | 49 | newParts.push(...parts); 50 | }); 51 | 52 | parsedTexts = newParts; 53 | }); 54 | 55 | // Remove _matched key. 56 | parsedTexts.forEach((parsedText) => delete(parsedText._matched)); 57 | 58 | return parsedTexts.filter(t => !!t.children); 59 | } 60 | 61 | // private 62 | 63 | /** 64 | * @param {Object} matchedPattern - pattern configuration of the pattern used to match the text 65 | * @param {RegExp} matchedPattern.pattern - pattern used to match the text 66 | * @param {String} text - Text matching the pattern 67 | * @param {String[]} text - Result of the RegExp.exec 68 | * @return {Object} props for the matched text 69 | */ 70 | getMatchedPart(matchedPattern, text, matches) { 71 | let props = {}; 72 | 73 | Object.keys(matchedPattern).forEach((key) => { 74 | if (key === 'pattern' || key === 'renderText') { return; } 75 | 76 | if (typeof matchedPattern[key] === 'function') { 77 | props[key] = () => matchedPattern[key](text); 78 | } else { 79 | props[key] = matchedPattern[key]; 80 | } 81 | }); 82 | 83 | let children = text; 84 | if (matchedPattern.renderText && typeof matchedPattern.renderText === 'function') { 85 | children = matchedPattern.renderText(text, matches); 86 | } 87 | 88 | return { 89 | ...props, 90 | children: children, 91 | _matched: true, 92 | }; 93 | } 94 | } 95 | 96 | export default TextExtraction; 97 | -------------------------------------------------------------------------------- /example/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right