From d36299ec3d1e03773edeeb5c8d97ececb5d754fb Mon Sep 17 00:00:00 2001 From: Kenneth Lynne Date: Mon, 3 Apr 2017 15:55:01 +0200 Subject: [PATCH] chore: merge latest from origin (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Request] Add prop for tintColor for ActionSheet in Actions #185 Add prop optionTintColor for ActionSheet options text * Update default optionTintColor to native color * Initial support tick messages * The message sent and delivered props have been removed from GiftedChat and are now read from the message state * Added possibility to override the behaviour of renderTicks, deleted nested function in renderTicks * Updated example and ticks on the right the time and ticks only appear for the sent messages * renderTick: Move the constant declaration to the top of the function * Expose GiftedAvatar * renderTick: Possibility to change style of the ticks passing the parameter tickStyle to GiftedChat * Change the new messages' ids Now are UUIDs generated with `node-uuid` and is possible to change this behavior using `messageIdGenerator` prop. * fixed chat height bug * Refactor utils into stand-alone functions * Improve readability of utils * Keep improving readability of utils * Add utils to root exports * Add prop onInputTextChanged on GiftedChat API and help fix #286 * Maintain backward compatibility with deprecation warning * Add eslint script (extends airbnb + react + react-native) Currently 506 errors, 7 warnings :D * Upgrade to eslint config v0.2.3 Removed a few unnecessarily strict rules, so we're down to 486 errors now. * Add react-native-lightbox for enlarging chat images * Add navigator to propTypes * Use a commit hash to fix Lightbox warning [Android] Currently the fix is unpublished. * Remove touch events and external keyboard dependency * Bump RN version * Remove commented code * Downgrade RN version * Enabling access to Library Example crashes when you want to choose an image via „choose from library“ . Fixed that. Example should work with this edit like intended. * Remove dependency from package.json * Update property Deprecated property true for keyboardShouldPersistTaps * Update property for Gifted Chat Deprecated property true for keyboardShouldPersistTaps * Add 'onPressActionButton' prop * Add 'imageProps' prop, passed through to * Use 'lightboxProps' instead of just 'navigator' * config ListView props support * fix warning for Avatar Warning: Failed prop type: Invalid prop `left` of type `object` supplied to `Avatar`, expected `number`. Bad object: { "left": { "borderWidth": 1, "borderColor": "#236CC3" } } * avoid listViewProps to override render methd and datasources * Add listViewPoprs description to README * add listViewProps to default props and propTypes * Downgrade RN version to original * Update prop validation for keyboardShouldPersistTaps * Update Readme * Update RN version to 0.40 * readme for 0.1.0 * bump npm versions * Improve send message performance * add missing props to inner message components * v0.1.0 * fix for npm dependency * node-uuid -> uuid * v0.1.1 * Create GiftedChatInteractionManager.js * Update GiftedChat.js * v0.1.2 * v0.1.3 * render avatar on top * Remove duplicated enableEmptySections * If the keyboard has been dismissed and the user sends a message, the text input tool bar can be rendered incorrectly due to the bottom offset * Allow minimum input tool bar height to be configurable in properties * Update Bubble.js * Export MessageContainer in GiftedChat * fix for _messageContainerRef === null * Bubble.js: Default value for prop bottomContainerStyle * Applying fixes per comments * rename @exponent to @expo * v0.1.4 * image source add number type * formatting * Merge remote-tracking branch 'FaridSafi/master' into fix-package-name # Conflicts: # README.md # package.json # src/GiftedChat.js # src/MessageContainer.js * add test command --- .eslintignore | 3 + .eslintrc | 22 +- README.md | 13 +- example/data/messages.js | 2 + .../ios/GiftedChat.xcodeproj/project.pbxproj | 242 ++++++++++++++++++ .../xcschemes/GiftedChat.xcscheme | 27 +- .../AppIcon.appiconset/Contents.json | 10 + example/ios/GiftedChat/Info.plist | 22 +- example/package.json | 4 +- package.json | 33 ++- src/Actions.js | 6 +- src/Avatar.js | 53 ++-- src/Bubble.js | 60 ++++- src/Composer.js | 36 ++- src/Day.js | 12 +- src/GiftedAvatar.js | 7 + src/GiftedChat.js | 203 +++++++-------- src/GiftedChatInteractionManager.js | 15 ++ src/InputToolbar.js | 4 + src/Message.js | 53 +--- src/MessageContainer.js | 4 + src/MessageImage.js | 27 +- src/utils.js | 31 +++ 23 files changed, 672 insertions(+), 217 deletions(-) create mode 100644 .eslintignore create mode 100644 src/GiftedChatInteractionManager.js create mode 100644 src/utils.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..355bd39fc --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules +android +ios diff --git a/.eslintrc b/.eslintrc index 8ac527fe3..3b80aa885 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,16 +1,12 @@ { - "plugins": [ - "react", - "react-native" - ], - "extends": ["eslint:recommended", "plugin:react/recommended"], - "parserOptions": { - "sourceType": "module", - "ecmaFeatures" : { - experimentalObjectRestSpread: true - } - }, + "extends": "cooperka/react-native", + "env": { - "node": true - } + "browser": true, + "jest": true + }, + + // Any rules here will override those from + // https://github.com/cooperka/eslint-config-cooperka. + "rules": {} } diff --git a/README.md b/README.md index 0f35056b6..648795a6a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ The most complete chat UI for React Native (formerly known as Gifted Messenger) ![](https://raw.githubusercontent.com/FaridSafi/react-native-gifted-chat/master/screenshots/gifted-chat-2.png) ## Dependency -React Native minimum version `0.29.0` +Use `0.0.10` for RN < `0.40.0` +Use `0.1.0` for RN >= `0.40.0` ## Installation `npm install react-native-gifted-chat --save` @@ -94,6 +95,7 @@ See [example/App.js](example/App.js) ## Props - **`messages`** _(Array)_ - messages to display +- **`messageIdGenerator`** _(Function)_ - generate id for new message. By default is a UUID v4 generated by [uuid](https://github.com/kelektiv/node-uuid). - **`user`** _(Object)_ - user sending the messages `{_id, name, avatar}` - **`onSend`** _(Function)_ - function to call when sending a message - **`locale`** _(String)_ - localize the dates @@ -104,10 +106,13 @@ See [example/App.js](example/App.js) - **`renderLoading`** _(Function)_ - render a loading view when initializing - **`renderLoadEarlier`** _(Function)_ - render the load earlier button - **`renderAvatar`** _(Function)_ - renders the message avatar +- **`renderAvatarOnTop`** _(Bool)_ - render the message avatar, on top of consecutive messages. The default value is `false`. - **`renderBubble`** _(Function)_ - render the message bubble - **`renderMessage`** _(Function)_ - render the message container - **`renderMessageText`** _(Function)_ - render the message text - **`renderMessageImage`** _(Function)_ - render the message image +- **`imageProps`** _(Object)_ - extra props to be passed to the [``](https://facebook.github.io/react-native/docs/image.html) component created by the default `renderMessageImage` +- **`lightboxProps`** _(Object)_ - extra props to be passed to the MessageImage's [Lightbox](https://github.com/oblador/react-native-lightbox) - **`renderCustomView`** _(Function)_ - render a custom view inside the bubble - **`renderDay`** _(Function)_ - render the day above a message - **`renderTime`** _(Function)_ - render the message time @@ -117,9 +122,13 @@ See [example/App.js](example/App.js) - **`renderComposer`** _(Function)_ - render the text input message composer - **`renderSend`** _(Function)_ - render the send button - **`renderAccessory`** _(Function)_ - renders a second line of actions below the message composer +- **`onPressActionButton`** _(Function)_ - callback to perform custom logic when the Action button is pressed (the default `actionSheet` will not be used) - **`renderSuggestions`** _(Function)_ - renders suggestions for composer - **`bottomOffset`** _(Integer)_ - distance of the chat from the bottom of the screen, useful if you display a tab bar - +- **`minInputToolbarHeight`** _(Integer)_ - minimum height of the input toolbar. The default value is `44`. +- **`listViewProps`** _(Object)_ - extra props to be passed to the [``](https://facebook.github.io/react-native/docs/listview.html), some props can not be override, see the code in `render` method of `MessageContainer` for detail +- **`keyboardShouldPersistTaps`** _(Enum)_ - determines when the keyboard should stay visible after a tap [``](https://facebook.github.io/react-native/docs/scrollview.html) +- **`onInputTextChanged`** _(Function)_ - function that will be called when input text changes ## Features - Custom components diff --git a/example/data/messages.js b/example/data/messages.js index f1f321b0a..07a753e4b 100644 --- a/example/data/messages.js +++ b/example/data/messages.js @@ -7,6 +7,8 @@ module.exports = [ _id: 1, name: 'Developer', }, + sent: true, + received: true, // location: { // latitude: 48.864601, // longitude: 2.398704 diff --git a/example/ios/GiftedChat.xcodeproj/project.pbxproj b/example/ios/GiftedChat.xcodeproj/project.pbxproj index fb3dbf6ca..a1ce3bf10 100644 --- a/example/ios/GiftedChat.xcodeproj/project.pbxproj +++ b/example/ios/GiftedChat.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 1E3A76611E1D8508006B7212 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E3A765E1E1D84FE006B7212 /* libRCTAnimation.a */; }; 75B54F941D4D2A5E00BAB2C4 /* libRCTCameraRoll.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 75B54F8A1D4D2A5200BAB2C4 /* libRCTCameraRoll.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -90,6 +91,111 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 1E3A76341E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A283A1D9B042B00D4039D; + remoteInfo = "RCTImage-tvOS"; + }; + 1E3A76381E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28471D9B043800D4039D; + remoteInfo = "RCTLinking-tvOS"; + }; + 1E3A763C1E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28541D9B044C00D4039D; + remoteInfo = "RCTNetwork-tvOS"; + }; + 1E3A76401E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28611D9B046600D4039D; + remoteInfo = "RCTSettings-tvOS"; + }; + 1E3A76441E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A287B1D9B048500D4039D; + remoteInfo = "RCTText-tvOS"; + }; + 1E3A76491E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28881D9B049200D4039D; + remoteInfo = "RCTWebSocket-tvOS"; + }; + 1E3A764D1E1D7EEE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28131D9B038B00D4039D; + remoteInfo = "React-tvOS"; + }; + 1E3A765D1E1D84FE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1E3A76581E1D84FE006B7212 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTAnimation; + }; + 1E3A765F1E1D84FE006B7212 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1E3A76581E1D84FE006B7212 /* RCTAnimation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 2D2A28201D9B03D100D4039D; + remoteInfo = "RCTAnimation-tvOS"; + }; + 1E4B83341E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3C059A1DE3340900C268FA; + remoteInfo = yoga; + }; + 1E4B83361E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3C06751DE3340C00C268FA; + remoteInfo = "yoga-tvOS"; + }; + 1E4B83381E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD9251DE5FBEC00167DC4; + remoteInfo = cxxreact; + }; + 1E4B833A1E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD9321DE5FBEE00167DC4; + remoteInfo = "cxxreact-tvOS"; + }; + 1E4B833C1E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD90B1DE5FBD600167DC4; + remoteInfo = jschelpers; + }; + 1E4B833E1E2FCBA700562C98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3D3CD9181DE5FBD800167DC4; + remoteInfo = "jschelpers-tvOS"; + }; 75B54F891D4D2A5200BAB2C4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 75B54F841D4D2A5100BAB2C4 /* RCTCameraRoll.xcodeproj */; @@ -133,6 +239,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = GiftedChat/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = GiftedChat/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 1E3A76581E1D84FE006B7212 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; 75B54F841D4D2A5100BAB2C4 /* RCTCameraRoll.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCameraRoll.xcodeproj; path = "../node_modules/react-native/Libraries/CameraRoll/RCTCameraRoll.xcodeproj"; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; @@ -151,6 +258,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1E3A76611E1D8508006B7212 /* libRCTAnimation.a in Frameworks */, 75B54F941D4D2A5E00BAB2C4 /* libRCTCameraRoll.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, @@ -188,6 +296,7 @@ isa = PBXGroup; children = ( 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, + 1E3A76351E1D7EEE006B7212 /* libRCTImage-tvOS.a */, ); name = Products; sourceTree = ""; @@ -196,6 +305,7 @@ isa = PBXGroup; children = ( 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, + 1E3A763D1E1D7EEE006B7212 /* libRCTNetwork-tvOS.a */, ); name = Products; sourceTree = ""; @@ -229,6 +339,7 @@ isa = PBXGroup; children = ( 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + 1E3A76411E1D7EEE006B7212 /* libRCTSettings-tvOS.a */, ); name = Products; sourceTree = ""; @@ -237,6 +348,7 @@ isa = PBXGroup; children = ( 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, + 1E3A764A1E1D7EEE006B7212 /* libRCTWebSocket-tvOS.a */, ); name = Products; sourceTree = ""; @@ -259,6 +371,22 @@ isa = PBXGroup; children = ( 146834041AC3E56700842450 /* libReact.a */, + 1E3A764E1E1D7EEE006B7212 /* libReact-tvOS.a */, + 1E4B83351E2FCBA700562C98 /* libyoga.a */, + 1E4B83371E2FCBA700562C98 /* libyoga.a */, + 1E4B83391E2FCBA700562C98 /* libcxxreact.a */, + 1E4B833B1E2FCBA700562C98 /* libcxxreact.a */, + 1E4B833D1E2FCBA700562C98 /* libjschelpers.a */, + 1E4B833F1E2FCBA700562C98 /* libjschelpers.a */, + ); + name = Products; + sourceTree = ""; + }; + 1E3A76591E1D84FE006B7212 /* Products */ = { + isa = PBXGroup; + children = ( + 1E3A765E1E1D84FE006B7212 /* libRCTAnimation.a */, + 1E3A76601E1D84FE006B7212 /* libRCTAnimation-tvOS.a */, ); name = Products; sourceTree = ""; @@ -275,6 +403,7 @@ isa = PBXGroup; children = ( 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + 1E3A76391E1D7EEE006B7212 /* libRCTLinking-tvOS.a */, ); name = Products; sourceTree = ""; @@ -282,6 +411,7 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 1E3A76581E1D84FE006B7212 /* RCTAnimation.xcodeproj */, 75B54F841D4D2A5100BAB2C4 /* RCTCameraRoll.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, @@ -301,6 +431,7 @@ isa = PBXGroup; children = ( 832341B51AAA6A8300B99B32 /* libRCTText.a */, + 1E3A76451E1D7EEE006B7212 /* libRCTText-tvOS.a */, ); name = Products; sourceTree = ""; @@ -396,6 +527,10 @@ ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; }, + { + ProductGroup = 1E3A76591E1D84FE006B7212 /* Products */; + ProjectRef = 1E3A76581E1D84FE006B7212 /* RCTAnimation.xcodeproj */; + }, { ProductGroup = 75B54F851D4D2A5100BAB2C4 /* Products */; ProjectRef = 75B54F841D4D2A5100BAB2C4 /* RCTCameraRoll.xcodeproj */; @@ -502,6 +637,111 @@ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E3A76351E1D7EEE006B7212 /* libRCTImage-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTImage-tvOS.a"; + remoteRef = 1E3A76341E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A76391E1D7EEE006B7212 /* libRCTLinking-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTLinking-tvOS.a"; + remoteRef = 1E3A76381E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A763D1E1D7EEE006B7212 /* libRCTNetwork-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTNetwork-tvOS.a"; + remoteRef = 1E3A763C1E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A76411E1D7EEE006B7212 /* libRCTSettings-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTSettings-tvOS.a"; + remoteRef = 1E3A76401E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A76451E1D7EEE006B7212 /* libRCTText-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTText-tvOS.a"; + remoteRef = 1E3A76441E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A764A1E1D7EEE006B7212 /* libRCTWebSocket-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTWebSocket-tvOS.a"; + remoteRef = 1E3A76491E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A764E1E1D7EEE006B7212 /* libReact-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 1E3A764D1E1D7EEE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A765E1E1D84FE006B7212 /* libRCTAnimation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAnimation.a; + remoteRef = 1E3A765D1E1D84FE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E3A76601E1D84FE006B7212 /* libRCTAnimation-tvOS.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = "libRCTAnimation-tvOS.a"; + remoteRef = 1E3A765F1E1D84FE006B7212 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B83351E2FCBA700562C98 /* libyoga.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libyoga.a; + remoteRef = 1E4B83341E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B83371E2FCBA700562C98 /* libyoga.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libyoga.a; + remoteRef = 1E4B83361E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B83391E2FCBA700562C98 /* libcxxreact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libcxxreact.a; + remoteRef = 1E4B83381E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B833B1E2FCBA700562C98 /* libcxxreact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libcxxreact.a; + remoteRef = 1E4B833A1E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B833D1E2FCBA700562C98 /* libjschelpers.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjschelpers.a; + remoteRef = 1E4B833C1E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1E4B833F1E2FCBA700562C98 /* libjschelpers.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjschelpers.a; + remoteRef = 1E4B833E1E2FCBA700562C98 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 75B54F8A1D4D2A5200BAB2C4 /* libRCTCameraRoll.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -642,6 +882,7 @@ "$(SRCROOT)/../node_modules/react-native/React/**", ); INFOPLIST_FILE = GiftedChat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", @@ -662,6 +903,7 @@ "$(SRCROOT)/../node_modules/react-native/React/**", ); INFOPLIST_FILE = GiftedChat/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/example/ios/GiftedChat.xcodeproj/xcshareddata/xcschemes/GiftedChat.xcscheme b/example/ios/GiftedChat.xcodeproj/xcshareddata/xcschemes/GiftedChat.xcscheme index 01068eea6..5092b679e 100644 --- a/example/ios/GiftedChat.xcodeproj/xcshareddata/xcschemes/GiftedChat.xcscheme +++ b/example/ios/GiftedChat.xcodeproj/xcshareddata/xcschemes/GiftedChat.xcscheme @@ -3,9 +3,23 @@ LastUpgradeVersion = "0620" version = "1.3"> + + + + + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -62,15 +76,18 @@ ReferencedContainer = "container:GiftedChat.xcodeproj"> + + @@ -86,10 +103,10 @@ diff --git a/example/ios/GiftedChat/Images.xcassets/AppIcon.appiconset/Contents.json b/example/ios/GiftedChat/Images.xcassets/AppIcon.appiconset/Contents.json index 118c98f74..b8236c653 100644 --- a/example/ios/GiftedChat/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/example/ios/GiftedChat/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", diff --git a/example/ios/GiftedChat/Info.plist b/example/ios/GiftedChat/Info.plist index e98ebb004..e4809163a 100644 --- a/example/ios/GiftedChat/Info.plist +++ b/example/ios/GiftedChat/Info.plist @@ -2,6 +2,10 @@ + NSPhotoLibraryUsageDescription + Access to your Gallery + NSCameraUsageDescription + Access to your Camera CFBundleDevelopmentRegion en CFBundleExecutable @@ -22,6 +26,21 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + + + NSLocationWhenInUseUsageDescription + + NSPhotoLibraryUsageDescription + GiftedChat needs access to your photo library UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -36,10 +55,7 @@ UIViewControllerBasedStatusBarAppearance - NSLocationWhenInUseUsageDescription - NSAppTransportSecurity - NSExceptionDomains diff --git a/example/package.json b/example/package.json index b672fc0bc..ee579ad55 100644 --- a/example/package.json +++ b/example/package.json @@ -7,8 +7,8 @@ "sync": "rm -rf ./node_modules/react-native-gifted-chat; sane '/usr/bin/rsync -v -a --exclude .git --exclude example --exclude __tests__ --exclude node_modules ../ ./node_modules/react-native-gifted-chat/' .. --glob='{**/*.json,**/*.js}'" }, "dependencies": { - "react": "15.2.1", - "react-native": "^0.30.0", + "react": "~15.4.0-rc.4", + "react-native": "^0.40.0", "react-native-camera-roll-picker": "^1.1.7", "react-native-gifted-chat": "file:../", "react-native-nav": "^1.1.4" diff --git a/package.json b/package.json index f7ad17ddb..e86dc0e1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-gifted-chat", - "version": "0.0.11", + "version": "0.1.4", "description": "The most complete chat UI for React Native", "main": "index.js", "engines": { @@ -27,20 +27,29 @@ "url": "https://github.com/FaridSafi/react-native-gifted-chat/issues" }, "homepage": "https://github.com/FaridSafi/react-native-gifted-chat#readme", + "scripts": { + "lint": "eslint . --ext .js,.jsx", + "test": "npm run lint" + }, "devDependencies": { - "eslint": "^3.2.2", - "eslint-plugin-react": "^6.0.0", - "eslint-plugin-react-native": "^2.0.0" + "eslint": "~3.12.2", + "eslint-config-airbnb": "~13.0.0", + "eslint-config-cooperka": "~0.2.3", + "eslint-plugin-import": "~2.2.0", + "eslint-plugin-jsx-a11y": "~2.2.3", + "eslint-plugin-react": "~6.8.0", + "eslint-plugin-react-native": "~2.2.1" }, "dependencies": { - "@exponent/react-native-action-sheet": "git+https://github.com/FaridSafi/react-native-action-sheet.git", + "@expo/react-native-action-sheet": "0.3.0", "lodash": "^4.16.4", - "md5": "^2.1.0", - "moment": "^2.13.0", - "react-native-communications": "^2.1.0", - "react-native-dismiss-keyboard": "^1.0.0", - "react-native-invertible-scroll-view": "^1.0.0", - "react-native-parsed-text": "^0.0.15", - "shallowequal": "^0.2.2" + "md5": "2.2.1", + "moment": "2.17.1", + "react-native-communications": "2.1.1", + "react-native-invertible-scroll-view": "1.0.0", + "react-native-lightbox": "oblador/react-native-lightbox#c84a8543d4511fe6a44c3d7820747c9c1bddd875", + "react-native-parsed-text": "0.0.16", + "shallowequal": "0.2.2", + "uuid": "3.0.1" } } diff --git a/src/Actions.js b/src/Actions.js index 7368e0178..6a5110890 100644 --- a/src/Actions.js +++ b/src/Actions.js @@ -18,6 +18,7 @@ export default class Actions extends React.Component { this.context.actionSheet().showActionSheetWithOptions({ options, cancelButtonIndex, + tintColor: this.props.optionTintColor }, (buttonIndex) => { let i = 0; @@ -54,7 +55,7 @@ export default class Actions extends React.Component { return ( {this.renderIcon()} @@ -91,6 +92,7 @@ Actions.contextTypes = { Actions.defaultProps = { onSend: () => {}, options: {}, + optionTintColor: '#007AFF', icon: null, containerStyle: {}, iconTextStyle: {}, @@ -99,7 +101,9 @@ Actions.defaultProps = { Actions.propTypes = { onSend: React.PropTypes.func, options: React.PropTypes.object, + optionTintColor: React.PropTypes.string, icon: React.PropTypes.func, + onPressActionButton: React.PropTypes.func, containerStyle: View.propTypes.style, iconTextStyle: Text.propTypes.style, }; diff --git a/src/Avatar.js b/src/Avatar.js index 85c4a459d..0c623246a 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -1,11 +1,7 @@ -import React from 'react'; -import { - Image, - StyleSheet, - View, -} from 'react-native'; - -import GiftedAvatar from './GiftedAvatar'; +import React from "react"; +import {Image, StyleSheet, View} from "react-native"; +import GiftedAvatar from "./GiftedAvatar"; +import {isSameUser, isSameDay, warnDeprecated} from "./utils"; export default class Avatar extends React.Component { renderAvatar() { @@ -22,7 +18,11 @@ export default class Avatar extends React.Component { } render() { - if (this.props.isSameUser(this.props.currentMessage, this.props.nextMessage) && this.props.isSameDay(this.props.currentMessage, this.props.nextMessage)) { + const renderAvatarOnTop = this.props.renderAvatarOnTop; + const messageToCompare = renderAvatarOnTop ? this.props.previousMessage : this.props.nextMessage; + const computedStyle = renderAvatarOnTop ? "onTop" : "onBottom" + + if (isSameUser(this.props.currentMessage, messageToCompare) && isSameDay(this.props.currentMessage, messageToCompare)) { return ( + {this.renderAvatar()} ); @@ -42,8 +43,12 @@ export default class Avatar extends React.Component { const styles = { left: StyleSheet.create({ container: { - marginRight: 8, + marginRight: 8 }, + onTop: { + alignSelf: "flex-start" + }, + onBottom: {}, image: { height: 36, width: 36, @@ -54,6 +59,10 @@ const styles = { container: { marginLeft: 8, }, + onTop: { + alignSelf: "flex-start" + }, + onBottom: {}, image: { height: 36, width: 36, @@ -63,8 +72,7 @@ const styles = { }; Avatar.defaultProps = { - isSameDay: () => {}, - isSameUser: () => {}, + renderAvatarOnTop: false, position: 'left', currentMessage: { user: null, @@ -72,14 +80,25 @@ Avatar.defaultProps = { nextMessage: {}, containerStyle: {}, imageStyle: {}, + //TODO: remove in next major release + isSameDay: warnDeprecated(isSameDay), + isSameUser: warnDeprecated(isSameUser) }; Avatar.propTypes = { - isSameDay: React.PropTypes.func, - isSameUser: React.PropTypes.func, + renderAvatarOnTop: React.PropTypes.bool, position: React.PropTypes.oneOf(['left', 'right']), currentMessage: React.PropTypes.object, nextMessage: React.PropTypes.object, - containerStyle: View.propTypes.style, - imageStyle: React.PropTypes.oneOfType([View.propTypes.style, Image.propTypes.style]), + containerStyle: React.PropTypes.shape({ + left: View.propTypes.style, + right: View.propTypes.style, + }), + imageStyle: React.PropTypes.shape({ + left: View.propTypes.style, + right: View.propTypes.style, + }), + //TODO: remove in next major release + isSameDay: React.PropTypes.func, + isSameUser: React.PropTypes.func }; diff --git a/src/Bubble.js b/src/Bubble.js index 9e1d27cd7..ea51977ae 100644 --- a/src/Bubble.js +++ b/src/Bubble.js @@ -1,5 +1,6 @@ import React from 'react'; import { + Text, Clipboard, StyleSheet, TouchableWithoutFeedback, @@ -10,6 +11,8 @@ import MessageText from './MessageText'; import MessageImage from './MessageImage'; import Time from './Time'; +import { isSameUser, isSameDay, warnDeprecated } from './utils'; + export default class Bubble extends React.Component { constructor(props) { super(props); @@ -17,14 +20,14 @@ export default class Bubble extends React.Component { } handleBubbleToNext() { - if (this.props.isSameUser(this.props.currentMessage, this.props.nextMessage) && this.props.isSameDay(this.props.currentMessage, this.props.nextMessage)) { + if (isSameUser(this.props.currentMessage, this.props.nextMessage) && isSameDay(this.props.currentMessage, this.props.nextMessage)) { return StyleSheet.flatten([styles[this.props.position].containerToNext, this.props.containerToNextStyle[this.props.position]]); } return null; } handleBubbleToPrevious() { - if (this.props.isSameUser(this.props.currentMessage, this.props.previousMessage) && this.props.isSameDay(this.props.currentMessage, this.props.previousMessage)) { + if (isSameUser(this.props.currentMessage, this.props.previousMessage) && isSameDay(this.props.currentMessage, this.props.previousMessage)) { return StyleSheet.flatten([styles[this.props.position].containerToPrevious, this.props.containerToPreviousStyle[this.props.position]]); } return null; @@ -52,6 +55,24 @@ export default class Bubble extends React.Component { return null; } + renderTicks() { + const {currentMessage} = this.props; + if (this.props.renderTicks) { + return this.props.renderTicks(currentMessage); + } + if (currentMessage.user._id !== this.props.user._id) { + return; + } + if (currentMessage.sent || currentMessage.received) { + return ( + + {currentMessage.sent && } + {currentMessage.received && } + + ) + } + } + renderTime() { if (this.props.currentMessage.createdAt) { const {containerStyle, wrapperStyle, ...timeProps} = this.props; @@ -108,7 +129,10 @@ export default class Bubble extends React.Component { {this.renderCustomView()} {this.renderMessageImage()} {this.renderMessageText()} - {this.renderTime()} + + {this.renderTime()} + {this.renderTicks()} + @@ -156,6 +180,19 @@ const styles = { borderTopRightRadius: 3, }, }), + bottom: { + flexDirection: 'row', + justifyContent: 'flex-end', + }, + tick: { + fontSize: 10, + backgroundColor: 'transparent', + color: 'white', + }, + tickView: { + flexDirection: 'row', + marginRight: 10, + } }; Bubble.contextTypes = { @@ -169,8 +206,6 @@ Bubble.defaultProps = { renderMessageText: null, renderCustomView: null, renderTime: null, - isSameUser: () => {}, - isSameDay: () => {}, position: 'left', currentMessage: { text: null, @@ -181,8 +216,13 @@ Bubble.defaultProps = { previousMessage: {}, containerStyle: {}, wrapperStyle: {}, + bottomContainerStyle: {}, + tickStyle: {}, containerToNextStyle: {}, containerToPreviousStyle: {}, + //TODO: remove in next major release + isSameDay: warnDeprecated(isSameDay), + isSameUser: warnDeprecated(isSameUser), }; Bubble.propTypes = { @@ -192,8 +232,6 @@ Bubble.propTypes = { renderMessageText: React.PropTypes.func, renderCustomView: React.PropTypes.func, renderTime: React.PropTypes.func, - isSameUser: React.PropTypes.func, - isSameDay: React.PropTypes.func, position: React.PropTypes.oneOf(['left', 'right']), currentMessage: React.PropTypes.object, nextMessage: React.PropTypes.object, @@ -206,6 +244,11 @@ Bubble.propTypes = { left: View.propTypes.style, right: View.propTypes.style, }), + bottomContainerStyle: React.PropTypes.shape({ + left: View.propTypes.style, + right: View.propTypes.style, + }), + tickStyle: Text.propTypes.style, containerToNextStyle: React.PropTypes.shape({ left: View.propTypes.style, right: View.propTypes.style, @@ -214,4 +257,7 @@ Bubble.propTypes = { left: View.propTypes.style, right: View.propTypes.style, }), + //TODO: remove in next major release + isSameDay: React.PropTypes.func, + isSameUser: React.PropTypes.func, }; diff --git a/src/Composer.js b/src/Composer.js index e0f004992..3433777f7 100644 --- a/src/Composer.js +++ b/src/Composer.js @@ -6,6 +6,21 @@ import { } from 'react-native'; export default class Composer extends React.Component { + onChange(e) { + const contentSize = e.nativeEvent.contentSize; + if (!this.contentSize) { + this.contentSize = contentSize; + this.props.onInputSizeChanged(this.contentSize); + } else if (this.contentSize.width !== contentSize.width || this.contentSize.height !== contentSize.height) { + this.contentSize = contentSize; + this.props.onInputSizeChanged(this.contentSize); + } + } + + onChangeText(text) { + this.props.onTextChanged(text); + } + render() { return ( { - this.props.onChange(e); - }} - style={[styles.textInput, this.props.textInputStyle, { - height: this.props.composerHeight, - }]} + + onChange={(e) => this.onChange(e)} + onChangeText={text => this.onChangeText(text)} + + style={[styles.textInput, this.props.textInputStyle, {height: this.props.composerHeight}]} + value={this.props.text} accessibilityLabel={this.props.text || this.props.placeholder} enablesReturnKeyAutomatically={true} @@ -47,7 +62,8 @@ const styles = StyleSheet.create({ }); Composer.defaultProps = { - onChange: () => {}, + onChange: () => { + }, composerHeight: Platform.select({ ios: 33, android: 41, @@ -58,6 +74,10 @@ Composer.defaultProps = { textInputProps: null, multiline: true, textInputStyle: {}, + onTextChanged: () => { + }, + onInputSizeChanged: () => { + }, }; Composer.propTypes = { @@ -67,6 +87,8 @@ Composer.propTypes = { placeholder: React.PropTypes.string, placeholderTextColor: React.PropTypes.string, textInputProps: React.PropTypes.object, + onTextChanged: React.PropTypes.func, + onInputSizeChanged: React.PropTypes.func, multiline: React.PropTypes.bool, textInputStyle: TextInput.propTypes.style, }; diff --git a/src/Day.js b/src/Day.js index 66d828f88..8b45c9a6e 100644 --- a/src/Day.js +++ b/src/Day.js @@ -7,9 +7,11 @@ import { import moment from 'moment/min/moment-with-locales.min'; +import { isSameDay, isSameUser, warnDeprecated } from './utils'; + export default class Day extends React.Component { render() { - if (!this.props.isSameDay(this.props.currentMessage, this.props.previousMessage)) { + if (!isSameDay(this.props.currentMessage, this.props.previousMessage)) { return ( @@ -52,7 +54,6 @@ Day.contextTypes = { }; Day.defaultProps = { - isSameDay: () => {}, currentMessage: { // TODO test if crash when createdAt === null createdAt: null, @@ -61,13 +62,18 @@ Day.defaultProps = { containerStyle: {}, wrapperStyle: {}, textStyle: {}, + //TODO: remove in next major release + isSameDay: warnDeprecated(isSameDay), + isSameUser: warnDeprecated(isSameUser), }; Day.propTypes = { - isSameDay: React.PropTypes.func, currentMessage: React.PropTypes.object, previousMessage: React.PropTypes.object, containerStyle: View.propTypes.style, wrapperStyle: View.propTypes.style, textStyle: Text.propTypes.style, + //TODO: remove in next major release + isSameDay: React.PropTypes.func, + isSameUser: React.PropTypes.func, }; diff --git a/src/GiftedAvatar.js b/src/GiftedAvatar.js index 2ee7cdf30..b359b6c39 100644 --- a/src/GiftedAvatar.js +++ b/src/GiftedAvatar.js @@ -55,6 +55,13 @@ export default class GiftedAvatar extends React.Component { style={[defaultStyles.avatarStyle, this.props.avatarStyle]} /> ); + } else if (typeof this.props.user.avatar === 'number') { + return ( + + ); } return null; } diff --git a/src/GiftedChat.js b/src/GiftedChat.js index 2da285ba1..82c4166ba 100644 --- a/src/GiftedChat.js +++ b/src/GiftedChat.js @@ -7,11 +7,12 @@ import { View, } from 'react-native'; -import ActionSheet from '@exponent/react-native-action-sheet'; -import dismissKeyboard from 'react-native-dismiss-keyboard'; +import ActionSheet from '@expo/react-native-action-sheet'; import moment from 'moment/min/moment-with-locales.min'; +import uuid from 'uuid'; import _ from 'lodash'; +import * as utils from './utils'; import Actions from './Actions'; import Avatar from './Avatar'; import Bubble from './Bubble'; @@ -25,6 +26,8 @@ import Message from './Message'; import MessageContainer from './MessageContainer'; import Send from './Send'; import Time from './Time'; +import GiftedAvatar from './GiftedAvatar'; +import GiftedChatInteractionManager from './GiftedChatInteractionManager'; // Min and max heights of ToolbarInput and Composer // Needed for Composer auto grow and ScrollView animation @@ -34,7 +37,6 @@ const MIN_COMPOSER_HEIGHT = Platform.select({ android: 41, }); const MAX_COMPOSER_HEIGHT = 100; -const MIN_INPUT_TOOLBAR_HEIGHT = 44; class GiftedChat extends React.Component { constructor(props) { @@ -45,30 +47,32 @@ class GiftedChat extends React.Component { this._keyboardHeight = 0; this._bottomOffset = 0; this._maxHeight = null; - this._touchStarted = false; this._isFirstLayout = true; - this._isTypingDisabled = false; this._locale = 'en'; this._messages = []; this.state = { isInitialized: false, // initialization will calculate maxHeight before rendering the chat + composerHeight: MIN_COMPOSER_HEIGHT, + messagesContainerHeight: null, + typingDisabled: false }; - this.onTouchStart = this.onTouchStart.bind(this); - this.onTouchMove = this.onTouchMove.bind(this); - this.onTouchEnd = this.onTouchEnd.bind(this); this.onKeyboardWillShow = this.onKeyboardWillShow.bind(this); this.onKeyboardWillHide = this.onKeyboardWillHide.bind(this); this.onKeyboardDidShow = this.onKeyboardDidShow.bind(this); this.onKeyboardDidHide = this.onKeyboardDidHide.bind(this); - this.onType = this.onType.bind(this); this.onSend = this.onSend.bind(this); this.getLocale = this.getLocale.bind(this); + this.onInputSizeChanged = this.onInputSizeChanged.bind(this); + this.onInputTextChanged = this.onInputTextChanged.bind(this); + this.onMainViewLayout = this.onMainViewLayout.bind(this); + this.onInitialLayoutViewLayout = this.onInitialLayoutViewLayout.bind(this); + this.invertibleScrollViewProps = { inverted: true, - keyboardShouldPersistTaps: 'always', + keyboardShouldPersistTaps: this.props.keyboardShouldPersistTaps, onTouchStart: this.onTouchStart, onTouchMove: this.onTouchMove, onTouchEnd: this.onTouchEnd, @@ -175,11 +179,13 @@ class GiftedChat extends React.Component { } setIsTypingDisabled(value) { - this._isTypingDisabled = value; + this.setState({ + typingDisabled: value + }); } getIsTypingDisabled() { - return this._isTypingDisabled; + return this.state.typingDisabled; } setIsMounted(value) { @@ -193,10 +199,7 @@ class GiftedChat extends React.Component { // TODO // setMinInputToolbarHeight getMinInputToolbarHeight() { - if (this.props.renderAccessory) { - return MIN_INPUT_TOOLBAR_HEIGHT * 2; - } - return MIN_INPUT_TOOLBAR_HEIGHT; + return this.props.renderAccessory ? this.props.minInputToolbarHeight * 2 : this.props.minInputToolbarHeight; } prepareMessagesContainerHeight(value) { @@ -217,10 +220,8 @@ class GiftedChat extends React.Component { duration: 210, }).start(); } else { - this.setState((previousState) => { - return { - messagesContainerHeight: newMessagesContainerHeight, - }; + this.setState({ + messagesContainerHeight: newMessagesContainerHeight, }); } } @@ -236,10 +237,8 @@ class GiftedChat extends React.Component { duration: 210, }).start(); } else { - this.setState((previousState) => { - return { - messagesContainerHeight: newMessagesContainerHeight, - }; + this.setState({ + messagesContainerHeight: newMessagesContainerHeight, }); } } @@ -259,28 +258,13 @@ class GiftedChat extends React.Component { } scrollToBottom(animated = true) { + if (this._messageContainerRef === null) { return } this._messageContainerRef.scrollTo({ y: 0, animated, }); } - onTouchStart() { - this._touchStarted = true; - } - - onTouchMove() { - this._touchStarted = false; - } - - // handle Tap event to dismiss keyboard - onTouchEnd() { - if (this._touchStarted === true) { - dismissKeyboard(); - } - this._touchStarted = false; - } - renderMessages() { const AnimatedView = this.props.isAnimated === true ? Animated.View : View; return ( @@ -329,7 +313,7 @@ class GiftedChat extends React.Component { ...message, user: this.props.user, createdAt: new Date(), - _id: 'temp-id-' + Math.round(Math.random() * 1000000), + _id: this.props.messageIdGenerator(), }; }); @@ -346,7 +330,7 @@ class GiftedChat extends React.Component { if (this.getIsMounted() === true) { this.setIsTypingDisabled(false); } - }, 200); + }, 100); } if (Platform.OS === 'android' && _.get(this.refs, 'inputToolbar.refs.composer.refs.textInput')) { // hack to remove autocomplete cache this.refs.inputToolbar.refs.composer.refs.textInput.setNativeProps({keyboardType:"email-address"}); @@ -355,12 +339,13 @@ class GiftedChat extends React.Component { } resetInputToolbar() { - this.setState((previousState) => { - return { - text: '', - composerHeight: MIN_COMPOSER_HEIGHT, - messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight() - this.getKeyboardHeight() + this.getBottomOffset()), - }; + if (this.textInput) { + this.textInput.clear(); + } + this.setState({ + text: '', + composerHeight: MIN_COMPOSER_HEIGHT, + messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight() - this.getKeyboardHeight() + this.getBottomOffset()), }); } @@ -368,39 +353,75 @@ class GiftedChat extends React.Component { return newComposerHeight + (this.getMinInputToolbarHeight() - MIN_COMPOSER_HEIGHT); } - onType(e) { - if (this.getIsTypingDisabled() === true) { + onInputSizeChanged(size) { + const newComposerHeight = Math.max(MIN_COMPOSER_HEIGHT, Math.min(MAX_COMPOSER_HEIGHT, size.height)); + const newMessagesContainerHeight = this.getMaxHeight() - this.calculateInputToolbarHeight(newComposerHeight) - this.getKeyboardHeight() + this.getBottomOffset(); + this.setState({ + composerHeight: newComposerHeight, + messagesContainerHeight: this.prepareMessagesContainerHeight(newMessagesContainerHeight), + }); + } + + onInputTextChanged(text) { + if (this.getIsTypingDisabled()) { return; } - - let newComposerHeight = null; - if (e.nativeEvent && e.nativeEvent.contentSize) { - newComposerHeight = Math.max(MIN_COMPOSER_HEIGHT, Math.min(MAX_COMPOSER_HEIGHT, e.nativeEvent.contentSize.height)); - } else { - newComposerHeight = MIN_COMPOSER_HEIGHT; + if (this.props.onInputTextChanged) { + this.props.onInputTextChanged(text); } + this.setState({text}); + } - const newMessagesContainerHeight = this.getMaxHeight() - this.calculateInputToolbarHeight(newComposerHeight) - this.getKeyboardHeight() + this.getBottomOffset(); - const newText = e.nativeEvent.text; - this.setState((previousState) => { - return { - text: newText, - composerHeight: newComposerHeight, - messagesContainerHeight: this.prepareMessagesContainerHeight(newMessagesContainerHeight), - }; + onInitialLayoutViewLayout(e) { + const layout = e.nativeEvent.layout; + if (layout.height <= 0) { + return; + } + this.setMaxHeight(layout.height); + GiftedChatInteractionManager.runAfterInteractions(() => { + this.setState({ + isInitialized: true, + text: '', + composerHeight: MIN_COMPOSER_HEIGHT, + messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight()), + }); }); } + onMainViewLayout(e) { + if (Platform.OS === 'android') { + // fix an issue when keyboard is dismissing during the initialization + const layout = e.nativeEvent.layout; + if (this.getMaxHeight() !== layout.height && this.getIsFirstLayout() === true) { + this.setMaxHeight(layout.height); + this.setState({ + messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight()), + }); + } + } + if (this.getIsFirstLayout() === true) { + this.setIsFirstLayout(false); + } + } + renderInputToolbar() { const inputToolbarProps = { ...this.props, text: this.state.text, composerHeight: Math.max(MIN_COMPOSER_HEIGHT, this.state.composerHeight), - onChange: this.onType, onSend: this.onSend, - ref: 'inputToolbar' + ref: 'inputToolbar', + onInputSizeChanged: this.onInputSizeChanged, + onTextChanged: this.onInputTextChanged, + textInputProps: { + ...this.props.textInputProps, + ref: textInput => this.textInput = textInput, + maxLength: this.getIsTypingDisabled() ? 0 : null + } }; - + if (this.getIsTypingDisabled()) { + inputToolbarProps.textInputProps.maxLength = 0; + } if (this.props.renderInputToolbar) { return this.props.renderInputToolbar(inputToolbarProps); } @@ -433,24 +454,7 @@ class GiftedChat extends React.Component { if (this.state.isInitialized === true) { return ( this._actionSheetRef = component}> - { - if (Platform.OS === 'android') { - // fix an issue when keyboard is dismissing during the initialization - const layout = e.nativeEvent.layout; - if (this.getMaxHeight() !== layout.height && this.getIsFirstLayout() === true) { - this.setMaxHeight(layout.height); - this.setState({ - messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight()), - }); - } - } - if (this.getIsFirstLayout() === true) { - this.setIsFirstLayout(false); - } - }} - > + {this.renderMessages()} {this.renderInputToolbar()} {this.renderSuggestions()} @@ -459,21 +463,7 @@ class GiftedChat extends React.Component { ); } return ( - { - const layout = e.nativeEvent.layout; - this.setMaxHeight(layout.height); - InteractionManager.runAfterInteractions(() => { - this.setState({ - isInitialized: true, - text: '', - composerHeight: MIN_COMPOSER_HEIGHT, - messagesContainerHeight: this.prepareMessagesContainerHeight(this.getMaxHeight() - this.getMinInputToolbarHeight()), - }); - }); - }} - > + {this.renderLoading()} ); @@ -509,6 +499,10 @@ GiftedChat.defaultProps = { ios: true, android: false, }), + keyboardShouldPersistTaps: Platform.select({ + ios: 'never', + android: 'always', + }), renderAccessory: null, renderActions: null, renderAvatar: null, @@ -529,12 +523,15 @@ GiftedChat.defaultProps = { renderSuggestions: null, user: {}, bottomOffset: 0, + minInputToolbarHeight: 44, isLoadingEarlier: false, + messageIdGenerator: () => uuid.v4() }; GiftedChat.propTypes = { messages: React.PropTypes.array, onSend: React.PropTypes.func, + onInputTextChanged: React.PropTypes.func, loadEarlier: React.PropTypes.bool, onLoadEarlier: React.PropTypes.func, locale: React.PropTypes.string, @@ -559,7 +556,10 @@ GiftedChat.propTypes = { renderSuggestions: React.PropTypes.func, user: React.PropTypes.object, bottomOffset: React.PropTypes.number, + minInputToolbarHeight: React.PropTypes.number, isLoadingEarlier: React.PropTypes.bool, + messageIdGenerator: React.PropTypes.func, + keyboardShouldPersistTaps: React.PropTypes.oneOf(['always', 'never', 'handled']), }; export { @@ -574,6 +574,9 @@ export { InputToolbar, LoadEarlier, Message, + MessageContainer, Send, Time, + GiftedAvatar, + utils }; diff --git a/src/GiftedChatInteractionManager.js b/src/GiftedChatInteractionManager.js new file mode 100644 index 000000000..0c081e3e9 --- /dev/null +++ b/src/GiftedChatInteractionManager.js @@ -0,0 +1,15 @@ +import { InteractionManager } from "react-native"; +export default { + ...InteractionManager, + runAfterInteractions: f => { + // ensure f get called, timeout at 500ms + // @gre workaround https://github.com/facebook/react-native/issues/8624 + let called = false; + const timeout = setTimeout(() => { called = true; f() }, 500); + InteractionManager.runAfterInteractions(() => { + if (called) return; + clearTimeout(timeout); + f(); + }); + } +}; diff --git a/src/InputToolbar.js b/src/InputToolbar.js index 40400bacb..9b0336920 100644 --- a/src/InputToolbar.js +++ b/src/InputToolbar.js @@ -6,11 +6,14 @@ import { import Composer from './Composer'; import Send from './Send'; +import Actions from './Actions'; export default class InputToolbar extends React.Component { renderActions() { if (this.props.renderActions) { return this.props.renderActions(this.props); + } else if (this.props.onPressActionButton) { + return ; } return null; } @@ -90,6 +93,7 @@ InputToolbar.propTypes = { renderActions: React.PropTypes.func, renderSend: React.PropTypes.func, renderComposer: React.PropTypes.func, + onPressActionButton: React.PropTypes.func, containerStyle: View.propTypes.style, primaryStyle: View.propTypes.style, accessoryStyle: View.propTypes.style, diff --git a/src/Message.js b/src/Message.js index 1df2e0ca3..659285bce 100644 --- a/src/Message.js +++ b/src/Message.js @@ -4,44 +4,26 @@ import { StyleSheet, } from 'react-native'; -import moment from 'moment'; - import Avatar from './Avatar'; import Bubble from './Bubble'; import Day from './Day'; -export default class Message extends React.Component { +import {isSameUser, isSameDay} from './utils'; - isSameDay(currentMessage = {}, diffMessage = {}) { - let diff = 0; - if (diffMessage.createdAt && currentMessage.createdAt) { - diff = Math.abs(moment(diffMessage.createdAt).startOf('day').diff(moment(currentMessage.createdAt).startOf('day'), 'days')); - } else { - diff = 1; - } - if (diff === 0) { - return true; - } - return false; - } +export default class Message extends React.Component { - isSameUser(currentMessage = {}, diffMessage = {}) { - if (diffMessage.user && currentMessage.user) { - if (diffMessage.user._id === currentMessage.user._id) { - return true; - } + getInnerComponentProps() { + const {containerStyle, ...props} = this.props; + return { + ...props, + isSameUser, + isSameDay } - return false; } renderDay() { if (this.props.currentMessage.createdAt) { - const {containerStyle, ...other} = this.props; - const dayProps = { - ...other, - isSameUser: this.isSameUser, - isSameDay: this.isSameDay, - }; + const dayProps = this.getInnerComponentProps(); if (this.props.renderDay) { return this.props.renderDay(dayProps); } @@ -51,12 +33,7 @@ export default class Message extends React.Component { } renderBubble() { - const {containerStyle, ...other} = this.props; - const bubbleProps = { - ...other, - isSameUser: this.isSameUser, - isSameDay: this.isSameDay, - }; + const bubbleProps = this.getInnerComponentProps(); if (this.props.renderBubble) { return this.props.renderBubble(bubbleProps); } @@ -65,13 +42,7 @@ export default class Message extends React.Component { renderAvatar() { if (this.props.user._id !== this.props.currentMessage.user._id) { - const {containerStyle, ...other} = this.props; - const avatarProps = { - ...other, - isSameUser: this.isSameUser, - isSameDay: this.isSameDay, - }; - + const avatarProps = this.getInnerComponentProps(); return ; } return null; @@ -82,7 +53,7 @@ export default class Message extends React.Component { {this.renderDay()} {this.props.position === 'left' ? this.renderAvatar() : null} {this.renderBubble()} diff --git a/src/MessageContainer.js b/src/MessageContainer.js index 2f7b9740a..acb0c24ec 100644 --- a/src/MessageContainer.js +++ b/src/MessageContainer.js @@ -146,6 +146,8 @@ export default class MessageContainer extends React.Component { initialListSize={20} pageSize={20} + {...this.props.listViewProps} + dataSource={this.state.dataSource} renderRow={this.renderRow} @@ -164,6 +166,7 @@ MessageContainer.defaultProps = { user: {}, renderFooter: null, renderMessage: null, + listViewProps: {}, onLoadEarlier: () => { }, }; @@ -174,4 +177,5 @@ MessageContainer.propTypes = { renderFooter: React.PropTypes.func, renderMessage: React.PropTypes.func, onLoadEarlier: React.PropTypes.func, + listViewProps: React.PropTypes.object, }; diff --git a/src/MessageImage.js b/src/MessageImage.js index c56358b2d..2d7d9e3a5 100644 --- a/src/MessageImage.js +++ b/src/MessageImage.js @@ -3,16 +3,28 @@ import { Image, StyleSheet, View, + Dimensions, } from 'react-native'; +import Lightbox from 'react-native-lightbox'; export default class MessageImage extends React.Component { render() { + const { width, height } = Dimensions.get('window'); + return ( - + + + ); } @@ -28,6 +40,9 @@ const styles = StyleSheet.create({ margin: 3, resizeMode: 'cover', }, + imageActive: { + resizeMode: 'contain', + }, }); MessageImage.defaultProps = { @@ -36,10 +51,14 @@ MessageImage.defaultProps = { }, containerStyle: {}, imageStyle: {}, + imageProps: {}, + lightboxProps: {}, }; MessageImage.propTypes = { currentMessage: React.PropTypes.object, containerStyle: View.propTypes.style, imageStyle: Image.propTypes.style, + imageProps: React.PropTypes.object, + lightboxProps: React.PropTypes.object, }; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 000000000..9ebfaed5a --- /dev/null +++ b/src/utils.js @@ -0,0 +1,31 @@ +import moment from 'moment'; + +const DEPRECATION_MESSAGE = 'isSameUser and isSameDay should be imported from the utils module instead of using the props functions'; + +export function isSameDay(currentMessage = {}, diffMessage = {}) { + + let currentCreatedAt = moment(currentMessage.createdAt); + let diffCreatedAt = moment(diffMessage.createdAt); + + if (!currentCreatedAt.isValid() || !diffCreatedAt.isValid()) { + return false; + } + + return currentCreatedAt.isSame(diffCreatedAt, 'day'); + +} + +export function isSameUser(currentMessage = {}, diffMessage = {}) { + + return !!(diffMessage.user && currentMessage.user && diffMessage.user._id === currentMessage.user._id); + +} + +export function warnDeprecated(fn) { + + return (...args) => { + console.warn(DEPRECATION_MESSAGE); + return fn(...args); + }; + +}