From c86e06afa0f5197a410cb82884f6c0c0255cbb0d Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 19 Jan 2018 10:38:14 -0200 Subject: [PATCH 01/11] @all & @here mention (#200) --- app/containers/MessageBox/index.js | 45 +++++++++++++++++++++++------- app/containers/MessageBox/style.js | 5 ++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index af1a196d5..be0542799 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -212,11 +212,21 @@ export default class MessageBox extends React.PureComponent { }); } + _getFixedMentions(keyword) { + if ('all'.indexOf(keyword) !== -1) { + this.users = [{ _id: -1, username: 'all', desc: 'all' }, ...this.users]; + } + if ('here'.indexOf(keyword) !== -1) { + this.users = [{ _id: -2, username: 'here', desc: 'active users' }, ...this.users]; + } + } + async _getUsers(keyword) { this.users = database.objects('users'); if (keyword) { this.users = this.users.filtered('username CONTAINS[c] $0', keyword); } + this._getFixedMentions(keyword); this.setState({ mentions: this.users.slice() }); const usernames = []; @@ -244,8 +254,9 @@ export default class MessageBox extends React.PureComponent { console.log('spotlight canceled'); } finally { delete this.oldPromise; - this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword); - this.setState({ mentions: this.users.slice() }); + this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(); + this._getFixedMentions(keyword); + this.setState({ mentions: this.users }); } } @@ -345,20 +356,34 @@ export default class MessageBox extends React.PureComponent { this.component.setNativeProps({ text: newText }); this.setState({ text: newText }); } - renderMentionItem = item => ( + renderFixedMentionItem = item => ( this._onPressMention(item)} > - - {item.username || item.name } + {item.username} + Notify {item.desc} in this room ) + renderMentionItem = (item) => { + if (item.username === 'all' || item.username === 'here') { + return this.renderFixedMentionItem(item); + } + return ( + this._onPressMention(item)} + > + + {item.username || item.name } + + ); + } renderEmoji() { const emojiContainer = ( diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/style.js index bee61609e..6bb79baa1 100644 --- a/app/containers/MessageBox/style.js +++ b/app/containers/MessageBox/style.js @@ -85,5 +85,10 @@ export default StyleSheet.create({ borderTopColor: '#ECECEC', borderTopWidth: 1, backgroundColor: '#fff' + }, + fixedMentionAvatar: { + fontWeight: 'bold', + textAlign: 'center', + width: 46 } }); From bf6b919cb383c233edd2364b1d9e9f3eef288b67 Mon Sep 17 00:00:00 2001 From: Saket Kumar <15ucs114@lnmiit.ac.in> Date: Tue, 23 Jan 2018 17:53:45 +0530 Subject: [PATCH 02/11] App perform a vibration feedback onLongPress (#213) --- android/app/src/main/AndroidManifest.xml | 1 + app/containers/MessageActions.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 67e24ad00..5308caeed 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + { this.ActionSheet.show(); + Vibration.vibrate(50); }); } else if (this.props.permalink !== nextProps.permalink && nextProps.permalink) { // copy permalink From 037caf5e174db0b6e1fcdfb5e1c753ae4aeeb6c1 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 25 Jan 2018 12:04:20 -0200 Subject: [PATCH 03/11] Message box issues (#218) --- android/app/src/main/AndroidManifest.xml | 1 - app/containers/MessageBox/index.js | 25 +++++++----------------- app/containers/MessageBox/style.js | 13 +++++------- app/views/RoomView/index.js | 14 ++++++------- package-lock.json | 5 +++++ package.json | 1 + 6 files changed, 25 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5308caeed..494507756 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature" /> - { const { start, end } = this.component._lastNativeSelection; const cursor = Math.max(start, end); - const text = this.component._lastNativeText; + const lastNativeText = this.component._lastNativeText; const regexp = /(#|@)([a-z._-]+)$/im; - const result = text.substr(0, cursor).match(regexp); + const result = lastNativeText.substr(0, cursor).match(regexp); if (!result) { return this.stopTrackingMention(); @@ -92,11 +93,6 @@ export default class MessageBox extends React.PureComponent { }); } - - onChangeText(text) { - this.setState({ text }); - } - get leftButtons() { const { editing } = this.props; if (editing) { @@ -147,10 +143,6 @@ export default class MessageBox extends React.PureComponent { return icons; } - updateSize = (height) => { - this.setState({ height: height + (Platform.OS === 'ios' ? 0 : 0) }); - } - addFile = () => { const options = { maxHeight: 1960, @@ -407,7 +399,6 @@ export default class MessageBox extends React.PureComponent { return ; } render() { - const { height } = this.state; return ( this.component = component} - style={[styles.textBoxInput, { height }]} + style={styles.textBoxInput} returnKeyType='default' blurOnSubmit={false} placeholder='New Message' onChangeText={text => this.onChangeText(text)} - onChange={event => this.onChange(event)} value={this.state.text} underlineColorAndroid='transparent' defaultValue='' multiline placeholderTextColor='#9EA2A8' - onContentSizeChange={e => this.updateSize(e.nativeEvent.contentSize.height)} /> {this.rightButtons} diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/style.js index 6bb79baa1..298a69ee6 100644 --- a/app/containers/MessageBox/style.js +++ b/app/containers/MessageBox/style.js @@ -9,8 +9,6 @@ export default StyleSheet.create({ alignItems: 'center', borderTopWidth: 1, borderTopColor: '#D8D8D8', - paddingHorizontal: 15, - paddingVertical: 15, zIndex: 2 }, safeAreaView: { @@ -23,14 +21,13 @@ export default StyleSheet.create({ flexGrow: 0 }, textBoxInput: { - paddingVertical: 0, - paddingHorizontal: 10, - textAlignVertical: 'top', + paddingHorizontal: 5, + textAlignVertical: 'center', maxHeight: 120, flexGrow: 1, width: 1, - paddingTop: 0, - paddingBottom: 0 + paddingTop: 15, + paddingBottom: 15 }, editing: { backgroundColor: '#fff5df' @@ -39,7 +36,7 @@ export default StyleSheet.create({ color: '#2F343D', fontSize: 20, textAlign: 'center', - paddingHorizontal: 5, + padding: 15, flex: 0 }, actionRow: { diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index c960c74c4..023dae62e 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, Button, SafeAreaView } from 'react-native'; +import { Text, View, Button, SafeAreaView, Platform } from 'react-native'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import equal from 'deep-equal'; +import KeyboardSpacer from 'react-native-keyboard-spacer'; import { ListView } from './ListView'; import * as actions from '../../actions'; @@ -16,14 +17,12 @@ import MessageActions from '../../containers/MessageActions'; import MessageErrorActions from '../../containers/MessageErrorActions'; import MessageBox from '../../containers/MessageBox'; import Typing from '../../containers/Typing'; -import KeyboardView from '../../presentation/KeyboardView'; import Header from '../../containers/Header'; import RoomsHeader from './Header'; import Banner from './banner'; import styles from './styles'; import debounce from '../../utils/debounce'; -import scrollPersistTaps from '../../utils/scrollPersistTaps'; const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); @@ -196,8 +195,7 @@ export default class RoomView extends React.Component { } render() { return ( - - + this.renderItem(item)} initialListSize={10} - {...scrollPersistTaps} + keyboardShouldPersistTaps='always' + keyboardDismissMode='on-drag' /> {this.renderFooter()} - + {Platform.OS === 'ios' ? : null} + ); } } diff --git a/package-lock.json b/package-lock.json index c2a1dc120..df02c28ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12542,6 +12542,11 @@ "react-native-iphone-x-helper": "1.0.1" } }, + "react-native-keyboard-spacer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/react-native-keyboard-spacer/-/react-native-keyboard-spacer-0.4.1.tgz", + "integrity": "sha1-RvGKMgQyCYol6p+on1FD3SVNMy0=" + }, "react-native-loading-spinner-overlay": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/react-native-loading-spinner-overlay/-/react-native-loading-spinner-overlay-0.5.2.tgz", diff --git a/package.json b/package.json index 688bc0f10..71bf39267 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-native-image-picker": "^0.26.7", "react-native-img-cache": "^1.5.2", "react-native-keyboard-aware-scroll-view": "^0.4.1", + "react-native-keyboard-spacer": "^0.4.1", "react-native-loading-spinner-overlay": "^0.5.2", "react-native-meteor": "^1.2.0", "react-native-modal": "^4.1.1", From 153cfecab5a7aa37cca3dd0527f027ebebacf8ce Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 30 Jan 2018 13:07:09 -0200 Subject: [PATCH 04/11] Keyboard dismiss (#219) * Fix keyboard scroll * * Input resize * Touchable input * keyboard dismiss * Remove unnecessary emoji picker when it's not in use --- app/containers/MessageBox/AnimatedContainer.js | 9 +++++++++ app/containers/Typing.js | 7 +++++-- app/containers/message/index.js | 7 ++++++- app/views/RoomView/index.js | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/containers/MessageBox/AnimatedContainer.js b/app/containers/MessageBox/AnimatedContainer.js index eb5cbbe47..688ccd591 100644 --- a/app/containers/MessageBox/AnimatedContainer.js +++ b/app/containers/MessageBox/AnimatedContainer.js @@ -12,6 +12,7 @@ export default class MessageBox extends React.PureComponent { constructor(props) { super(props); this.animatedBottom = new Animated.Value(0); + this.state = { render: false }; } componentWillReceiveProps(nextProps) { @@ -25,6 +26,7 @@ export default class MessageBox extends React.PureComponent { } show() { + this.setState({ render: true }); this.animatedBottom.setValue(0); Animated.timing(this.animatedBottom, { toValue: 1, @@ -39,6 +41,9 @@ export default class MessageBox extends React.PureComponent { duration: 300, useNativeDriver: true }).start(); + setTimeout(() => { + this.setState({ render: false }); + }, 300); } render() { @@ -47,6 +52,10 @@ export default class MessageBox extends React.PureComponent { outputRange: [0, -this.props.messageboxHeight - 200] }); + if (!this.state.render) { + return null; + } + return ( { + Keyboard.dismiss(); + } get usersTyping() { const users = this.props.usersTyping.filter(_username => this.props.username !== _username); return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : ''; } render() { - return ({this.usersTyping}); + return ( this.onPress()}>{this.usersTyping}); } } diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 7ab119870..f57bb5e14 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TouchableHighlight, Text, TouchableOpacity, Animated } from 'react-native'; +import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; @@ -64,6 +64,10 @@ export default class Message extends React.Component { return this.props.item._updatedAt.toGMTString() !== nextProps.item._updatedAt.toGMTString() || this.props.item.status !== nextProps.item.status; } + onPress = () => { + Keyboard.dismiss(); + } + onLongPress() { const { item } = this.props; this.props.actionsShow(JSON.parse(JSON.stringify(item))); @@ -181,6 +185,7 @@ export default class Message extends React.Component { return ( this.onPress()} onLongPress={() => this.onLongPress()} disabled={this.isDeleted() || this.hasError()} underlayColor='#FFFFFF' diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 023dae62e..94bbd4f12 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -209,7 +209,7 @@ export default class RoomView extends React.Component { renderRow={item => this.renderItem(item)} initialListSize={10} keyboardShouldPersistTaps='always' - keyboardDismissMode='on-drag' + keyboardDismissMode='none' /> {this.renderFooter()} From 9ea5c1b765cc7785e164fb0da1501f68cea0e767 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 30 Jan 2018 17:48:26 -0200 Subject: [PATCH 05/11] Reactions (#214) * * Tracking emoji * Fixed users/rooms regex tracking * Autocomplete emoji * Toggle reaction * 'User have reacted' style * Show who have reacted onLongPress * Vibration onLongPress --- android/app/src/main/res/values/styles.xml | 1 + android/gradle.properties | 1 + app/actions/actionsTypes.js | 3 +- app/actions/messages.js | 7 + .../{ => EmojiPicker}/CustomEmoji.js | 8 +- app/containers/EmojiPicker/EmojiCategory.js | 80 + .../{MessageBox => }/EmojiPicker/TabBar.js | 12 +- .../EmojiPicker/categories.js | 2 +- .../{MessageBox => }/EmojiPicker/index.js | 112 +- .../{MessageBox => }/EmojiPicker/styles.js | 15 +- app/containers/MessageActions.js | 19 +- .../MessageBox/EmojiPicker/EmojiCategory.js | 49 - app/containers/MessageBox/index.js | 97 +- app/containers/MessageBox/style.js | 13 +- app/containers/message/Emoji.js | 26 + app/containers/message/Markdown.js | 2 +- app/containers/message/ReactionsModal.js | 124 + app/containers/message/Url.js | 11 +- app/containers/message/index.js | 105 +- app/containers/message/styles.js | 30 + app/emojis.js | 2833 +++++++++++++++++ app/lib/realm.js | 22 +- app/lib/rocketchat.js | 6 + app/reducers/messages.js | 9 +- app/views/RoomView/ListView.js | 57 +- app/views/RoomView/ReactionPicker.js | 60 + app/views/RoomView/index.js | 138 +- app/views/RoomView/styles.js | 10 +- package-lock.json | 49 +- package.json | 4 +- 30 files changed, 3652 insertions(+), 253 deletions(-) rename app/containers/{ => EmojiPicker}/CustomEmoji.js (77%) create mode 100644 app/containers/EmojiPicker/EmojiCategory.js rename app/containers/{MessageBox => }/EmojiPicker/TabBar.js (66%) rename app/containers/{MessageBox => }/EmojiPicker/categories.js (75%) rename app/containers/{MessageBox => }/EmojiPicker/index.js (50%) rename app/containers/{MessageBox => }/EmojiPicker/styles.js (70%) delete mode 100644 app/containers/MessageBox/EmojiPicker/EmojiCategory.js create mode 100644 app/containers/message/Emoji.js create mode 100644 app/containers/message/ReactionsModal.js create mode 100644 app/emojis.js create mode 100644 app/views/RoomView/ReactionPicker.js diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 319eb0ca1..654ec9502 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -3,6 +3,7 @@ diff --git a/android/gradle.properties b/android/gradle.properties index 1fd964e90..732c56e3e 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -18,3 +18,4 @@ # org.gradle.parallel=true android.useDeprecatedNdk=true +# VERSIONCODE=999999999 diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index b4123946b..4e67c7478 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -55,7 +55,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [ 'TOGGLE_PIN_SUCCESS', 'TOGGLE_PIN_FAILURE', 'SET_INPUT', - 'CLEAR_INPUT' + 'CLEAR_INPUT', + 'TOGGLE_REACTION_PICKER' ]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ ...defaultTypes, diff --git a/app/actions/messages.js b/app/actions/messages.js index 29c1b4ca9..ed9bf4971 100644 --- a/app/actions/messages.js +++ b/app/actions/messages.js @@ -176,3 +176,10 @@ export function clearInput() { type: types.MESSAGES.CLEAR_INPUT }; } + +export function toggleReactionPicker(message) { + return { + type: types.MESSAGES.TOGGLE_REACTION_PICKER, + message + }; +} diff --git a/app/containers/CustomEmoji.js b/app/containers/EmojiPicker/CustomEmoji.js similarity index 77% rename from app/containers/CustomEmoji.js rename to app/containers/EmojiPicker/CustomEmoji.js index 166e065ab..5988296f3 100644 --- a/app/containers/CustomEmoji.js +++ b/app/containers/EmojiPicker/CustomEmoji.js @@ -6,19 +6,21 @@ import { connect } from 'react-redux'; @connect(state => ({ baseUrl: state.settings.Site_Url })) -export default class extends React.PureComponent { +export default class extends React.Component { static propTypes = { baseUrl: PropTypes.string.isRequired, emoji: PropTypes.object.isRequired, style: PropTypes.object } - + shouldComponentUpdate() { + return false; + } render() { const { baseUrl, emoji, style } = this.props; return ( ); } diff --git a/app/containers/EmojiPicker/EmojiCategory.js b/app/containers/EmojiPicker/EmojiCategory.js new file mode 100644 index 000000000..0fc3c65d7 --- /dev/null +++ b/app/containers/EmojiPicker/EmojiCategory.js @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Text, View, TouchableOpacity, Platform } from 'react-native'; +import { emojify } from 'react-emojione'; +import { responsive } from 'react-native-responsive-ui'; +import styles from './styles'; +import CustomEmoji from './CustomEmoji'; + + +const emojisPerRow = Platform.OS === 'ios' ? 8 : 9; + +const renderEmoji = (emoji, size) => { + if (emoji.isCustom) { + return ; + } + return ( + + {emojify(`:${ emoji }:`, { output: 'unicode' })} + + ); +}; + + +const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve)); + +@responsive +export default class EmojiCategory extends React.Component { + static propTypes = { + emojis: PropTypes.any, + window: PropTypes.any, + onEmojiSelected: PropTypes.func, + emojisPerRow: PropTypes.number, + width: PropTypes.number + }; + constructor(props) { + super(props); + const { width, height } = this.props.window; + + this.size = Math.min(this.props.width || width, height) / (this.props.emojisPerRow || emojisPerRow); + this.emojis = []; + } + componentWillMount() { + this.emojis = this.props.emojis.slice(0, emojisPerRow * 3).map(item => this.renderItem(item, this.size)); + } + async componentDidMount() { + const array = this.props.emojis; + const temparray = []; + let i; + let j; + const chunk = emojisPerRow * 3; + for (i = chunk, j = array.length; i < j; i += chunk) { + temparray.push(array.slice(i, i + chunk)); + } + temparray.forEach(async(items) => { + await nextFrame(); + this.emojis = this.emojis.concat(items.map(item => this.renderItem(item, this.size))); + this.forceUpdate(); + await nextFrame(); + }); + } + + shouldComponentUpdate() { + return false; + } + + renderItem(emoji, size) { + return ( + this.props.onEmojiSelected(emoji)} + > + {renderEmoji(emoji, size)} + ); + } + + render() { + return {this.emojis}; + } +} diff --git a/app/containers/MessageBox/EmojiPicker/TabBar.js b/app/containers/EmojiPicker/TabBar.js similarity index 66% rename from app/containers/MessageBox/EmojiPicker/TabBar.js rename to app/containers/EmojiPicker/TabBar.js index 9bc131a46..fb23b30ea 100644 --- a/app/containers/MessageBox/EmojiPicker/TabBar.js +++ b/app/containers/EmojiPicker/TabBar.js @@ -7,15 +7,21 @@ export default class extends React.PureComponent { static propTypes = { goToPage: PropTypes.func, activeTab: PropTypes.number, - tabs: PropTypes.array + tabs: PropTypes.array, + tabEmojiStyle: PropTypes.object } render() { return ( {this.props.tabs.map((tab, i) => ( - this.props.goToPage(i)} style={styles.tab}> - {tab} + this.props.goToPage(i)} + style={styles.tab} + > + {tab} {this.props.activeTab === i ? : } ))} diff --git a/app/containers/MessageBox/EmojiPicker/categories.js b/app/containers/EmojiPicker/categories.js similarity index 75% rename from app/containers/MessageBox/EmojiPicker/categories.js rename to app/containers/EmojiPicker/categories.js index 341c6a83e..a95f67cf6 100644 --- a/app/containers/MessageBox/EmojiPicker/categories.js +++ b/app/containers/EmojiPicker/categories.js @@ -1,4 +1,4 @@ -const list = ['Frequently Used', 'Custom', 'Smileys & People', 'Animals & Nature', 'Food & Drink', 'Activities', 'Travel & Places', 'Objects', 'Symbols', 'Flags']; +const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags']; const tabs = [ { tabLabel: '🕒', diff --git a/app/containers/MessageBox/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js similarity index 50% rename from app/containers/MessageBox/EmojiPicker/index.js rename to app/containers/EmojiPicker/index.js index fbddf4c78..4209b5cc8 100644 --- a/app/containers/MessageBox/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -1,35 +1,32 @@ -import 'string.fromcodepoint'; -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ScrollView, View } from 'react-native'; +import { ScrollView } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; -import emojiDatasource from 'emoji-datasource/emoji.json'; import _ from 'lodash'; -import { groupBy, orderBy } from 'lodash/collection'; -import { mapValues } from 'lodash/object'; +import { emojify } from 'react-emojione'; import TabBar from './TabBar'; import EmojiCategory from './EmojiCategory'; import styles from './styles'; import categories from './categories'; -import scrollPersistTaps from '../../../utils/scrollPersistTaps'; -import database from '../../../lib/realm'; +import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import database from '../../lib/realm'; +import { emojisByCategory } from '../../emojis'; -const charFromUtf16 = utf16 => String.fromCodePoint(...utf16.split('-').map(u => `0x${ u }`)); -const charFromEmojiObj = obj => charFromUtf16(obj.unified); +const scrollProps = { + keyboardShouldPersistTaps: 'always' +}; -const filteredEmojis = emojiDatasource.filter(e => parseFloat(e.added_in) < 10.0); -const groupedAndSorted = groupBy(orderBy(filteredEmojis, 'sort_order'), 'category'); -const emojisByCategory = mapValues(groupedAndSorted, group => group.map(charFromEmojiObj)); - -export default class extends PureComponent { +export default class extends Component { static propTypes = { - onEmojiSelected: PropTypes.func + onEmojiSelected: PropTypes.func, + tabEmojiStyle: PropTypes.object, + emojisPerRow: PropTypes.number, + width: PropTypes.number }; constructor(props) { super(props); this.state = { - categories: categories.list.slice(0, 1), frequentlyUsed: [], customEmojis: [] }; @@ -38,16 +35,22 @@ export default class extends PureComponent { this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this); this.updateCustomEmojis = this.updateCustomEmojis.bind(this); } + // + // shouldComponentUpdate(nextProps) { + // return false; + // } componentWillMount() { this.frequentlyUsed.addListener(this.updateFrequentlyUsed); this.customEmojis.addListener(this.updateCustomEmojis); this.updateFrequentlyUsed(); this.updateCustomEmojis(); + setTimeout(() => this.setState({ show: true }), 100); } componentWillUnmount() { - clearTimeout(this._timeout); + this.frequentlyUsed.removeAllListeners(); + this.customEmojis.removeAllListeners(); } onEmojiSelected(emoji) { @@ -58,10 +61,11 @@ export default class extends PureComponent { }); this.props.onEmojiSelected(`:${ emoji.content }:`); } else { - const content = emoji.codePointAt(0).toString(); + const content = emoji; const count = this._getFrequentlyUsedCount(content); this._addFrequentlyUsed({ content, count, isCustom: false }); - this.props.onEmojiSelected(emoji); + const shortname = `:${ emoji }:`; + this.props.onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname); } } _addFrequentlyUsed = (emoji) => { @@ -78,22 +82,17 @@ export default class extends PureComponent { if (item.isCustom) { return item; } - return String.fromCodePoint(item.content); + return emojify(`${ item.content }`, { output: 'unicode' }); }); this.setState({ frequentlyUsed }); } updateCustomEmojis() { - const customEmojis = _.map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true })); + const customEmojis = _.map(this.customEmojis.slice(), item => + ({ content: item.name, extension: item.extension, isCustom: true })); this.setState({ customEmojis }); } - loadNextCategory() { - if (this.state.categories.length < categories.list.length) { - this.setState({ categories: categories.list.slice(0, this.state.categories.length + 1) }); - } - } - renderCategory(category, i) { let emojis = []; if (i === 0) { @@ -104,40 +103,39 @@ export default class extends PureComponent { emojis = emojisByCategory[category]; } return ( - - this.onEmojiSelected(emoji)} - finishedLoading={() => { this._timeout = setTimeout(this.loadNextCategory.bind(this), 100); }} - /> - + this.onEmojiSelected(emoji)} + style={styles.categoryContainer} + size={this.props.emojisPerRow} + width={this.props.width} + /> ); } render() { - const scrollProps = { - keyboardShouldPersistTaps: 'always' - }; + if (!this.state.show) { + return null; + } return ( - - } - contentProps={scrollProps} - > - { - _.map(categories.tabs, (tab, i) => ( - - {this.renderCategory(tab.category, i)} - - )) - } - - + // + } + contentProps={scrollProps} + > + { + categories.tabs.map((tab, i) => ( + + {this.renderCategory(tab.category, i)} + + )) + } + + // ); } } diff --git a/app/containers/MessageBox/EmojiPicker/styles.js b/app/containers/EmojiPicker/styles.js similarity index 70% rename from app/containers/MessageBox/EmojiPicker/styles.js rename to app/containers/EmojiPicker/styles.js index a4ffdb598..038d11b20 100644 --- a/app/containers/MessageBox/EmojiPicker/styles.js +++ b/app/containers/EmojiPicker/styles.js @@ -1,7 +1,4 @@ -import { StyleSheet, Dimensions, Platform } from 'react-native'; - -const { width } = Dimensions.get('window'); -const EMOJI_SIZE = width / (Platform.OS === 'ios' ? 8 : 9); +import { StyleSheet } from 'react-native'; export default StyleSheet.create({ container: { @@ -45,18 +42,16 @@ export default StyleSheet.create({ categoryInner: { flexWrap: 'wrap', flexDirection: 'row', - alignItems: 'center' + alignItems: 'center', + justifyContent: 'flex-start', + flex: 1 }, categoryEmoji: { - fontSize: EMOJI_SIZE - 14, color: 'black', - height: EMOJI_SIZE, - width: EMOJI_SIZE, + backgroundColor: 'transparent', textAlign: 'center' }, customCategoryEmoji: { - height: EMOJI_SIZE - 8, - width: EMOJI_SIZE - 8, margin: 4 } }); diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index 56b33fcd5..c7ed5d2c4 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -13,7 +13,8 @@ import { permalinkClear, togglePinRequest, setInput, - actionsHide + actionsHide, + toggleReactionPicker } from '../actions/messages'; import { showToast } from '../utils/info'; @@ -39,7 +40,8 @@ import { showToast } from '../utils/info'; permalinkRequest: message => dispatch(permalinkRequest(message)), permalinkClear: () => dispatch(permalinkClear()), togglePinRequest: message => dispatch(togglePinRequest(message)), - setInput: message => dispatch(setInput(message)) + setInput: message => dispatch(setInput(message)), + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) }) ) export default class MessageActions extends React.Component { @@ -58,6 +60,7 @@ export default class MessageActions extends React.Component { togglePinRequest: PropTypes.func.isRequired, setInput: PropTypes.func.isRequired, permalink: PropTypes.string, + toggleReactionPicker: PropTypes.func.isRequired, Message_AllowDeleting: PropTypes.bool, Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, Message_AllowEditing: PropTypes.bool, @@ -119,6 +122,11 @@ export default class MessageActions extends React.Component { this.options.push(actionMessage.pinned ? 'Unpin' : 'Pin'); this.PIN_INDEX = this.options.length - 1; } + // Reaction + if (!this.isRoomReadOnly()) { + this.options.push('Add Reaction'); + this.REACTION_INDEX = this.options.length - 1; + } // Delete if (this.allowDelete(nextProps)) { this.options.push('Delete'); @@ -275,6 +283,10 @@ export default class MessageActions extends React.Component { this.props.permalinkRequest(this.props.actionMessage); } + handleReaction() { + this.props.toggleReactionPicker(this.props.actionMessage); + } + handleActionPress = (actionIndex) => { switch (actionIndex) { case this.REPLY_INDEX: @@ -298,6 +310,9 @@ export default class MessageActions extends React.Component { case this.PIN_INDEX: this.handlePin(); break; + case this.REACTION_INDEX: + this.handleReaction(); + break; case this.DELETE_INDEX: this.handleDelete(); break; diff --git a/app/containers/MessageBox/EmojiPicker/EmojiCategory.js b/app/containers/MessageBox/EmojiPicker/EmojiCategory.js deleted file mode 100644 index e4c648b1b..000000000 --- a/app/containers/MessageBox/EmojiPicker/EmojiCategory.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Text, View, TouchableOpacity, StyleSheet } from 'react-native'; -import styles from './styles'; -import CustomEmoji from '../../CustomEmoji'; - -export default class extends React.PureComponent { - static propTypes = { - emojis: PropTypes.any, - finishedLoading: PropTypes.func, - onEmojiSelected: PropTypes.func - }; - - componentDidMount() { - this.props.finishedLoading(); - } - - renderEmoji = (emoji) => { - if (emoji.isCustom) { - const style = StyleSheet.flatten(styles.customCategoryEmoji); - return ; - } - return ( - - {emoji} - - ); - } - - render() { - const { emojis } = this.props; - return ( - - - {emojis.map(emoji => - ( - this.props.onEmojiSelected(emoji)} - > - {this.renderEmoji(emoji)} - - ))} - - - ); - } -} diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index 9fd862569..b5f8aa3f2 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity, Keyboard } from 'react-native'; +import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity, Keyboard, StyleSheet } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; import ImagePicker from 'react-native-image-picker'; import { connect } from 'react-redux'; +import { emojify } from 'react-emojione'; import { userTyping } from '../../actions/room'; import RocketChat from '../../lib/rocketchat'; import { editRequest, editCancel, clearInput } from '../../actions/messages'; @@ -11,11 +12,14 @@ import styles from './style'; import MyIcon from '../icons'; import database from '../../lib/realm'; import Avatar from '../Avatar'; +import CustomEmoji from '../EmojiPicker/CustomEmoji'; import AnimatedContainer from './AnimatedContainer'; -import EmojiPicker from './EmojiPicker'; +import EmojiPicker from '../EmojiPicker'; import scrollPersistTaps from '../../utils/scrollPersistTaps'; +import { emojis } from '../../emojis'; const MENTIONS_TRACKING_TYPE_USERS = '@'; +const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; const onlyUnique = function onlyUnique(value, index, self) { return self.indexOf(({ _id }) => value._id === _id) === index; @@ -54,10 +58,13 @@ export default class MessageBox extends React.PureComponent { text: '', mentions: [], showMentionsContainer: false, - showEmojiContainer: false + showEmojiContainer: false, + trackingType: '' }; this.users = []; this.rooms = []; + this.emojis = []; + this.customEmojis = []; } componentWillReceiveProps(nextProps) { if (this.props.message !== nextProps.message && nextProps.message.msg) { @@ -80,7 +87,7 @@ export default class MessageBox extends React.PureComponent { const lastNativeText = this.component._lastNativeText; - const regexp = /(#|@)([a-z._-]+)$/im; + const regexp = /(#|@|:)([a-z0-9._-]+)$/im; const result = lastNativeText.substr(0, cursor).match(regexp); @@ -176,7 +183,10 @@ export default class MessageBox extends React.PureComponent { this.setState({ text: '' }); } async openEmoji() { - await this.setState({ showEmojiContainer: !this.state.showEmojiContainer }); + await this.setState({ + showEmojiContainer: true, + showMentionsContainer: false + }); Keyboard.dismiss(); } closeEmoji() { @@ -292,25 +302,41 @@ export default class MessageBox extends React.PureComponent { } } + _getEmojis(keyword) { + if (keyword) { + this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4); + this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4); + const mergedEmojis = [...this.customEmojis, ...this.emojis]; + this.setState({ mentions: mergedEmojis }); + } + } + stopTrackingMention() { this.setState({ showMentionsContainer: false, - mentions: [] + mentions: [], + trackingType: '' }); this.users = []; this.rooms = []; + this.customEmojis = []; + this.emojis = []; } identifyMentionKeyword(keyword, type) { - this.updateMentions(keyword, type); this.setState({ - showMentionsContainer: true + showMentionsContainer: true, + showEmojiContainer: false, + trackingType: type }); + this.updateMentions(keyword, type); } updateMentions = (keyword, type) => { if (type === MENTIONS_TRACKING_TYPE_USERS) { this._getUsers(keyword); + } else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) { + this._getEmojis(keyword); } else { this._getRooms(keyword); } @@ -323,10 +349,12 @@ export default class MessageBox extends React.PureComponent { const cursor = Math.max(start, end); - const regexp = /([a-z._-]+)$/im; + const regexp = /([a-z0-9._-]+)$/im; const result = msg.substr(0, cursor).replace(regexp, ''); - const text = `${ result }${ item.username || item.name } ${ msg.slice(cursor) }`; + const mentionName = this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? + `${ item.name || item }:` : (item.username || item.name); + const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`; this.component.setNativeProps({ text }); this.setState({ text }); this.component.focus(); @@ -357,6 +385,26 @@ export default class MessageBox extends React.PureComponent { Notify {item.desc} in this room ) + renderMentionEmoji = (item) => { + if (item.name) { + return ( + + ); + } + return ( + + {emojify(`:${ item }:`, { output: 'unicode' })} + + ); + } renderMentionItem = (item) => { if (item.username === 'all' || item.username === 'here') { return this.renderFixedMentionItem(item); @@ -366,13 +414,22 @@ export default class MessageBox extends React.PureComponent { style={styles.mentionItem} onPress={() => this._onPressMention(item)} > - - {item.username || item.name } + {this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ? + [ + this.renderMentionEmoji(item), + :{ item.name || item }: + ] + : [ + , + { item.username || item.name } + ] + } ); } @@ -386,17 +443,17 @@ export default class MessageBox extends React.PureComponent { return ; } renderMentions() { - const usersList = ( + const list = ( this.renderMentionItem(item)} - keyExtractor={item => item._id} + keyExtractor={item => item._id || item} {...scrollPersistTaps} /> ); const { showMentionsContainer, messageboxHeight } = this.state; - return ; + return ; } render() { return ( diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/style.js index 298a69ee6..98395ce4f 100644 --- a/app/containers/MessageBox/style.js +++ b/app/containers/MessageBox/style.js @@ -1,4 +1,4 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; const MENTION_HEIGHT = 50; @@ -83,6 +83,17 @@ export default StyleSheet.create({ borderTopWidth: 1, backgroundColor: '#fff' }, + mentionItemCustomEmoji: { + margin: 8, + width: 30, + height: 30 + }, + mentionItemEmoji: { + width: 46, + height: 36, + fontSize: Platform.OS === 'ios' ? 30 : 25, + textAlign: 'center' + }, fixedMentionAvatar: { fontWeight: 'bold', textAlign: 'center', diff --git a/app/containers/message/Emoji.js b/app/containers/message/Emoji.js new file mode 100644 index 000000000..95c0489a9 --- /dev/null +++ b/app/containers/message/Emoji.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Text } from 'react-native'; +import PropTypes from 'prop-types'; +import { emojify } from 'react-emojione'; +import CustomEmoji from '../EmojiPicker/CustomEmoji'; + +export default class Emoji extends React.PureComponent { + static propTypes = { + content: PropTypes.string, + standardEmojiStyle: PropTypes.object, + customEmojiStyle: PropTypes.object, + customEmojis: PropTypes.object.isRequired + }; + render() { + const { + content, standardEmojiStyle, customEmojiStyle, customEmojis + } = this.props; + const parsedContent = content.replace(/^:|:$/g, ''); + const emojiExtension = customEmojis[parsedContent]; + if (emojiExtension) { + const emoji = { extension: emojiExtension, content: parsedContent }; + return ; + } + return { emojify(`${ content }`, { output: 'unicode' }) }; + } +} diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 3fab10a45..14b82f6d7 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -5,7 +5,7 @@ import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line import SimpleMarkdown from 'simple-markdown'; import { emojify } from 'react-emojione'; import styles from './styles'; -import CustomEmoji from '../CustomEmoji'; +import CustomEmoji from '../EmojiPicker/CustomEmoji'; const BlockCode = ({ node, state }) => ( { + const count = item.usernames.length; + let usernames = item.usernames.slice(0, 3) + .map(username => (username.value === this.props.user.username ? 'you' : username.value)).join(', '); + if (count > 3) { + usernames = `${ usernames } and more ${ count - 3 }`; + } else { + usernames = usernames.replace(/,(?=[^,]*$)/, ' and'); + } + return ( + + + + + + + {count === 1 ? '1 person' : `${ count } people`} reacted + + { usernames } + + + ); + } + + render() { + const { + isVisible, onClose, reactions + } = this.props; + return ( + + + + + Reactions + + + + this.renderItem(item)} + keyExtractor={item => item.emoji} + /> + + + ); + } +} diff --git a/app/containers/message/Url.js b/app/containers/message/Url.js index 6fe6cdddb..5ec48a999 100644 --- a/app/containers/message/Url.js +++ b/app/containers/message/Url.js @@ -52,10 +52,13 @@ const Url = ({ url }) => { return ( onPress(url.url)} style={styles.button}> - + {url.image ? + + : null + } {url.title} {url.description} diff --git a/app/containers/message/index.js b/app/containers/message/index.js index f57bb5e14..840cfa0b2 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard } from 'react-native'; +import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard, StyleSheet, Vibration } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; +import equal from 'deep-equal'; -import { actionsShow, errorActionsShow } from '../../actions/messages'; +import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; import Image from './Image'; import User from './User'; import Avatar from '../Avatar'; @@ -14,6 +15,8 @@ import Video from './Video'; import Markdown from './Markdown'; import Url from './Url'; import Reply from './Reply'; +import ReactionsModal from './ReactionsModal'; +import Emoji from './Emoji'; import messageStatus from '../../constants/messagesStatus'; import styles from './styles'; @@ -26,11 +29,13 @@ const flex = { flexDirection: 'row', flex: 1 }; customEmojis: state.customEmojis }), dispatch => ({ actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), - errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)) + errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)), + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) })) export default class Message extends React.Component { static propTypes = { item: PropTypes.object.isRequired, + reactions: PropTypes.object.isRequired, baseUrl: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired, message: PropTypes.object.isRequired, @@ -39,7 +44,15 @@ export default class Message extends React.Component { actionsShow: PropTypes.func, errorActionsShow: PropTypes.func, animate: PropTypes.bool, - customEmojis: PropTypes.object + customEmojis: PropTypes.object, + toggleReactionPicker: PropTypes.func, + onReactionPress: PropTypes.func + } + + constructor(props) { + super(props); + this.state = { reactionsModal: false }; + this.onClose = this.onClose.bind(this); } componentWillMount() { @@ -60,7 +73,13 @@ export default class Message extends React.Component { } } - shouldComponentUpdate(nextProps) { + shouldComponentUpdate(nextProps, nextState) { + if (!equal(this.props.reactions, nextProps.reactions)) { + return true; + } + if (this.state.reactionsModal !== nextState.reactionsModal) { + return true; + } return this.props.item._updatedAt.toGMTString() !== nextProps.item._updatedAt.toGMTString() || this.props.item.status !== nextProps.item.status; } @@ -69,13 +88,22 @@ export default class Message extends React.Component { } onLongPress() { - const { item } = this.props; - this.props.actionsShow(JSON.parse(JSON.stringify(item))); + this.props.actionsShow(this.parseMessage()); } onErrorPress() { - const { item } = this.props; - this.props.errorActionsShow(JSON.parse(JSON.stringify(item))); + this.props.errorActionsShow(this.parseMessage()); + } + + onReactionPress(emoji) { + this.props.onReactionPress(emoji, this.props.item._id); + } + onClose() { + this.setState({ reactionsModal: false }); + } + onReactionLongPress() { + this.setState({ reactionsModal: true }); + Vibration.vibrate(50); } getInfoMessage() { @@ -105,11 +133,12 @@ export default class Message extends React.Component { return message; } + parseMessage = () => JSON.parse(JSON.stringify(this.props.item)); + isInfoMessage() { return ['r', 'au', 'ru', 'ul', 'uj', 'rm', 'user-muted', 'user-unmuted', 'message_pinned'].includes(this.props.item.t); } - isDeleted() { return this.props.item.t === 'rm'; } @@ -165,9 +194,50 @@ export default class Message extends React.Component { ); } + renderReaction(reaction) { + const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1; + const reactedContainerStyle = reacted ? { borderColor: '#bde1fe', backgroundColor: '#f3f9ff' } : {}; + const reactedCount = reacted ? { color: '#4fb0fc' } : {}; + return ( + this.onReactionPress(reaction.emoji)} + onLongPress={() => this.onReactionLongPress()} + key={reaction.emoji} + > + + + { reaction.usernames.length } + + + ); + } + + renderReactions() { + if (this.props.item.reactions.length === 0) { + return null; + } + return ( + + {this.props.item.reactions.map(reaction => this.renderReaction(reaction))} + this.props.toggleReactionPicker(this.parseMessage())} + key='add-reaction' + style={styles.reactionContainer} + > + + + + ); + } + render() { const { - item, message, editing, baseUrl + item, message, editing, baseUrl, customEmojis } = this.props; const marginLeft = this._visibility.interpolate({ @@ -181,7 +251,7 @@ export default class Message extends React.Component { const username = item.alias || item.u.username; const isEditing = message._id === item._id && editing; - const accessibilityLabel = `Message from ${ item.alias || item.u.username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; + const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; return ( + {this.state.reactionsModal ? + + : null + } ); diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 6236adf06..7752dea76 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -33,5 +33,35 @@ export default StyleSheet.create({ borderWidth: 1, borderRadius: 5, padding: 5 + }, + reactionsContainer: { + flexDirection: 'row', + flexWrap: 'wrap' + }, + reactionContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: 3, + borderWidth: 1, + borderColor: '#cccccc', + borderRadius: 4, + marginRight: 5, + marginBottom: 5, + height: 23, + width: 35 + }, + reactionCount: { + fontSize: 12, + marginLeft: 2, + fontWeight: '600', + color: '#aaaaaa' + }, + reactionEmoji: { + fontSize: 12 + }, + reactionCustomEmoji: { + width: 15, + height: 15 } }); diff --git a/app/emojis.js b/app/emojis.js new file mode 100644 index 000000000..b7bff1957 --- /dev/null +++ b/app/emojis.js @@ -0,0 +1,2833 @@ +export const emojisByCategory = { + people: [ + 'grinning', + 'grimacing', + 'grin', + 'joy', + 'smiley', + 'smile', + 'sweat_smile', + 'laughing', + 'innocent', + 'wink', + 'blush', + 'slight_smile', + 'upside_down', + 'relaxed', + 'yum', + 'relieved', + 'heart_eyes', + 'kissing_heart', + 'kissing', + 'kissing_smiling_eyes', + 'kissing_closed_eyes', + 'stuck_out_tongue_winking_eye', + 'stuck_out_tongue_closed_eyes', + 'stuck_out_tongue', + 'money_mouth', + 'nerd', + 'sunglasses', + 'hugging', + 'smirk', + 'no_mouth', + 'neutral_face', + 'expressionless', + 'unamused', + 'rolling_eyes', + 'thinking', + 'flushed', + 'disappointed', + 'worried', + 'angry', + 'rage', + 'pensive', + 'confused', + 'slight_frown', + 'frowning2', + 'persevere', + 'confounded', + 'tired_face', + 'weary', + 'triumph', + 'open_mouth', + 'scream', + 'fearful', + 'cold_sweat', + 'hushed', + 'frowning', + 'anguished', + 'cry', + 'disappointed_relieved', + 'sleepy', + 'sweat', + 'sob', + 'dizzy_face', + 'astonished', + 'zipper_mouth', + 'mask', + 'thermometer_face', + 'head_bandage', + 'sleeping', + 'zzz', + 'poop', + 'smiling_imp', + 'imp', + 'japanese_ogre', + 'japanese_goblin', + 'skull', + 'ghost', + 'alien', + 'robot', + 'smiley_cat', + 'smile_cat', + 'joy_cat', + 'heart_eyes_cat', + 'smirk_cat', + 'kissing_cat', + 'scream_cat', + 'crying_cat_face', + 'pouting_cat', + 'raised_hands', + 'clap', + 'wave', + 'thumbsup', + 'thumbsdown', + 'punch', + 'fist', + 'v', + 'ok_hand', + 'raised_hand', + 'open_hands', + 'muscle', + 'pray', + 'point_up', + 'point_up_2', + 'point_down', + 'point_left', + 'point_right', + 'middle_finger', + 'hand_splayed', + 'metal', + 'vulcan', + 'writing_hand', + 'nail_care', + 'lips', + 'tongue', + 'ear', + 'nose', + 'eye', + 'eyes', + 'bust_in_silhouette', + 'busts_in_silhouette', + 'speaking_head', + 'baby', + 'boy', + 'girl', + 'man', + 'woman', + 'person_with_blond_hair', + 'older_man', + 'older_woman', + 'man_with_gua_pi_mao', + 'man_with_turban', + 'cop', + 'construction_worker', + 'guardsman', + 'spy', + 'santa', + 'angel', + 'princess', + 'bride_with_veil', + 'walking', + 'runner', + 'dancer', + 'dancers', + 'couple', + 'two_men_holding_hands', + 'two_women_holding_hands', + 'bow', + 'information_desk_person', + 'no_good', + 'ok_woman', + 'raising_hand', + 'person_with_pouting_face', + 'person_frowning', + 'haircut', + 'massage', + 'couple_with_heart', + 'couple_ww', + 'couple_mm', + 'couplekiss', + 'kiss_ww', + 'kiss_mm', + 'family', + 'family_mwg', + 'family_mwgb', + 'family_mwbb', + 'family_mwgg', + 'family_wwb', + 'family_wwg', + 'family_wwgb', + 'family_wwbb', + 'family_wwgg', + 'family_mmb', + 'family_mmg', + 'family_mmgb', + 'family_mmbb', + 'family_mmgg', + 'womans_clothes', + 'shirt', + 'jeans', + 'necktie', + 'dress', + 'bikini', + 'kimono', + 'lipstick', + 'kiss', + 'footprints', + 'high_heel', + 'sandal', + 'boot', + 'mans_shoe', + 'athletic_shoe', + 'womans_hat', + 'tophat', + 'helmet_with_cross', + 'mortar_board', + 'crown', + 'school_satchel', + 'pouch', + 'purse', + 'handbag', + 'briefcase', + 'eyeglasses', + 'dark_sunglasses', + 'ring', + 'closed_umbrella', + 'cowboy', + 'clown', + 'nauseated_face', + 'rofl', + 'drooling_face', + 'lying_face', + 'sneezing_face', + 'prince', + 'man_in_tuxedo', + 'mrs_claus', + 'face_palm', + 'shrug', + 'selfie', + 'man_dancing', + 'call_me', + 'raised_back_of_hand', + 'left_facing_fist', + 'right_facing_fist', + 'handshake', + 'fingers_crossed', + 'pregnant_woman' + ], + nature: [ + 'dog', + 'cat', + 'mouse', + 'hamster', + 'rabbit', + 'bear', + 'panda_face', + 'koala', + 'tiger', + 'lion_face', + 'cow', + 'pig', + 'pig_nose', + 'frog', + 'octopus', + 'monkey_face', + 'see_no_evil', + 'hear_no_evil', + 'speak_no_evil', + 'monkey', + 'chicken', + 'penguin', + 'bird', + 'baby_chick', + 'hatching_chick', + 'hatched_chick', + 'wolf', + 'boar', + 'horse', + 'unicorn', + 'bee', + 'bug', + 'snail', + 'beetle', + 'ant', + 'spider', + 'scorpion', + 'crab', + 'snake', + 'turtle', + 'tropical_fish', + 'fish', + 'blowfish', + 'dolphin', + 'whale', + 'whale2', + 'crocodile', + 'leopard', + 'tiger2', + 'water_buffalo', + 'ox', + 'cow2', + 'dromedary_camel', + 'camel', + 'elephant', + 'goat', + 'ram', + 'sheep', + 'racehorse', + 'pig2', + 'rat', + 'mouse2', + 'rooster', + 'turkey', + 'dove', + 'dog2', + 'poodle', + 'cat2', + 'rabbit2', + 'chipmunk', + 'feet', + 'dragon', + 'dragon_face', + 'cactus', + 'christmas_tree', + 'evergreen_tree', + 'deciduous_tree', + 'palm_tree', + 'seedling', + 'herb', + 'shamrock', + 'four_leaf_clover', + 'bamboo', + 'tanabata_tree', + 'leaves', + 'fallen_leaf', + 'maple_leaf', + 'ear_of_rice', + 'hibiscus', + 'sunflower', + 'rose', + 'tulip', + 'blossom', + 'cherry_blossom', + 'bouquet', + 'mushroom', + 'chestnut', + 'jack_o_lantern', + 'shell', + 'spider_web', + 'earth_americas', + 'earth_africa', + 'earth_asia', + 'full_moon', + 'waning_gibbous_moon', + 'last_quarter_moon', + 'waning_crescent_moon', + 'new_moon', + 'waxing_crescent_moon', + 'first_quarter_moon', + 'waxing_gibbous_moon', + 'new_moon_with_face', + 'full_moon_with_face', + 'first_quarter_moon_with_face', + 'last_quarter_moon_with_face', + 'sun_with_face', + 'crescent_moon', + 'star', + 'star2', + 'dizzy', + 'sparkles', + 'comet', + 'sunny', + 'white_sun_small_cloud', + 'partly_sunny', + 'white_sun_cloud', + 'white_sun_rain_cloud', + 'cloud', + 'cloud_rain', + 'thunder_cloud_rain', + 'cloud_lightning', + 'zap', + 'fire', + 'boom', + 'snowflake', + 'cloud_snow', + 'snowman2', + 'snowman', + 'wind_blowing_face', + 'dash', + 'cloud_tornado', + 'fog', + 'umbrella2', + 'umbrella', + 'droplet', + 'sweat_drops', + 'ocean', + 'eagle', + 'duck', + 'bat', + 'shark', + 'owl', + 'fox', + 'butterfly', + 'deer', + 'gorilla', + 'lizard', + 'rhino', + 'wilted_rose', + 'shrimp', + 'squid' + ], + food: [ + 'green_apple', + 'apple', + 'pear', + 'tangerine', + 'lemon', + 'banana', + 'watermelon', + 'grapes', + 'strawberry', + 'melon', + 'cherries', + 'peach', + 'pineapple', + 'tomato', + 'eggplant', + 'hot_pepper', + 'corn', + 'sweet_potato', + 'honey_pot', + 'bread', + 'cheese', + 'poultry_leg', + 'meat_on_bone', + 'fried_shrimp', + 'cooking', + 'hamburger', + 'fries', + 'hotdog', + 'pizza', + 'spaghetti', + 'taco', + 'burrito', + 'ramen', + 'stew', + 'fish_cake', + 'sushi', + 'bento', + 'curry', + 'rice_ball', + 'rice', + 'rice_cracker', + 'oden', + 'dango', + 'shaved_ice', + 'ice_cream', + 'icecream', + 'cake', + 'birthday', + 'custard', + 'candy', + 'lollipop', + 'chocolate_bar', + 'popcorn', + 'doughnut', + 'cookie', + 'beer', + 'beers', + 'wine_glass', + 'cocktail', + 'tropical_drink', + 'champagne', + 'sake', + 'tea', + 'coffee', + 'baby_bottle', + 'fork_and_knife', + 'fork_knife_plate', + 'croissant', + 'avocado', + 'cucumber', + 'bacon', + 'potato', + 'carrot', + 'french_bread', + 'salad', + 'shallow_pan_of_food', + 'stuffed_flatbread', + 'champagne_glass', + 'tumbler_glass', + 'spoon', + 'egg', + 'milk', + 'peanuts', + 'kiwi', + 'pancakes' + ], + activity: [ + 'soccer', + 'basketball', + 'football', + 'baseball', + 'tennis', + 'volleyball', + 'rugby_football', + '8ball', + 'golf', + 'golfer', + 'ping_pong', + 'badminton', + 'hockey', + 'field_hockey', + 'cricket', + 'ski', + 'skier', + 'snowboarder', + 'ice_skate', + 'bow_and_arrow', + 'fishing_pole_and_fish', + 'rowboat', + 'swimmer', + 'surfer', + 'bath', + 'basketball_player', + 'lifter', + 'bicyclist', + 'mountain_bicyclist', + 'horse_racing', + 'levitate', + 'trophy', + 'running_shirt_with_sash', + 'medal', + 'military_medal', + 'reminder_ribbon', + 'rosette', + 'ticket', + 'tickets', + 'performing_arts', + 'art', + 'circus_tent', + 'microphone', + 'headphones', + 'musical_score', + 'musical_keyboard', + 'saxophone', + 'trumpet', + 'guitar', + 'violin', + 'clapper', + 'video_game', + 'space_invader', + 'dart', + 'game_die', + 'slot_machine', + 'bowling', + 'cartwheel', + 'juggling', + 'wrestlers', + 'boxing_glove', + 'martial_arts_uniform', + 'water_polo', + 'handball', + 'goal', + 'fencer', + 'first_place', + 'second_place', + 'third_place', + 'drum' + ], + travel: [ + 'red_car', + 'taxi', + 'blue_car', + 'bus', + 'trolleybus', + 'race_car', + 'police_car', + 'ambulance', + 'fire_engine', + 'minibus', + 'truck', + 'articulated_lorry', + 'tractor', + 'motorcycle', + 'bike', + 'rotating_light', + 'oncoming_police_car', + 'oncoming_bus', + 'oncoming_automobile', + 'oncoming_taxi', + 'aerial_tramway', + 'mountain_cableway', + 'suspension_railway', + 'railway_car', + 'train', + 'monorail', + 'bullettrain_side', + 'bullettrain_front', + 'light_rail', + 'mountain_railway', + 'steam_locomotive', + 'train2', + 'metro', + 'tram', + 'station', + 'helicopter', + 'airplane_small', + 'airplane', + 'airplane_departure', + 'airplane_arriving', + 'sailboat', + 'motorboat', + 'speedboat', + 'ferry', + 'cruise_ship', + 'rocket', + 'satellite_orbital', + 'seat', + 'anchor', + 'construction', + 'fuelpump', + 'busstop', + 'vertical_traffic_light', + 'traffic_light', + 'checkered_flag', + 'ship', + 'ferris_wheel', + 'roller_coaster', + 'carousel_horse', + 'construction_site', + 'foggy', + 'tokyo_tower', + 'factory', + 'fountain', + 'rice_scene', + 'mountain', + 'mountain_snow', + 'mount_fuji', + 'volcano', + 'japan', + 'camping', + 'tent', + 'park', + 'motorway', + 'railway_track', + 'sunrise', + 'sunrise_over_mountains', + 'desert', + 'beach', + 'island', + 'city_sunset', + 'city_dusk', + 'cityscape', + 'night_with_stars', + 'bridge_at_night', + 'milky_way', + 'stars', + 'sparkler', + 'fireworks', + 'rainbow', + 'homes', + 'european_castle', + 'japanese_castle', + 'stadium', + 'statue_of_liberty', + 'house', + 'house_with_garden', + 'house_abandoned', + 'office', + 'department_store', + 'post_office', + 'european_post_office', + 'hospital', + 'bank', + 'hotel', + 'convenience_store', + 'school', + 'love_hotel', + 'wedding', + 'classical_building', + 'church', + 'mosque', + 'synagogue', + 'kaaba', + 'shinto_shrine', + 'shopping_cart', + 'scooter', + 'motor_scooter', + 'canoe' + ], + objects: [ + 'watch', + 'iphone', + 'calling', + 'computer', + 'keyboard', + 'desktop', + 'printer', + 'mouse_three_button', + 'trackball', + 'joystick', + 'compression', + 'minidisc', + 'floppy_disk', + 'cd', + 'dvd', + 'vhs', + 'camera', + 'camera_with_flash', + 'video_camera', + 'movie_camera', + 'projector', + 'film_frames', + 'telephone_receiver', + 'telephone', + 'pager', + 'fax', + 'tv', + 'radio', + 'microphone2', + 'level_slider', + 'control_knobs', + 'stopwatch', + 'timer', + 'alarm_clock', + 'clock', + 'hourglass_flowing_sand', + 'hourglass', + 'satellite', + 'battery', + 'electric_plug', + 'bulb', + 'flashlight', + 'candle', + 'wastebasket', + 'oil', + 'money_with_wings', + 'dollar', + 'yen', + 'euro', + 'pound', + 'moneybag', + 'credit_card', + 'gem', + 'scales', + 'wrench', + 'hammer', + 'hammer_pick', + 'tools', + 'pick', + 'nut_and_bolt', + 'gear', + 'chains', + 'gun', + 'bomb', + 'knife', + 'dagger', + 'crossed_swords', + 'shield', + 'smoking', + 'skull_crossbones', + 'coffin', + 'urn', + 'amphora', + 'crystal_ball', + 'prayer_beads', + 'barber', + 'alembic', + 'telescope', + 'microscope', + 'hole', + 'pill', + 'syringe', + 'thermometer', + 'label', + 'bookmark', + 'toilet', + 'shower', + 'bathtub', + 'key', + 'key2', + 'couch', + 'sleeping_accommodation', + 'bed', + 'door', + 'bellhop', + 'frame_photo', + 'map', + 'beach_umbrella', + 'moyai', + 'shopping_bags', + 'balloon', + 'flags', + 'ribbon', + 'gift', + 'confetti_ball', + 'tada', + 'dolls', + 'wind_chime', + 'crossed_flags', + 'izakaya_lantern', + 'envelope', + 'envelope_with_arrow', + 'incoming_envelope', + 'e-mail', + 'love_letter', + 'postbox', + 'mailbox_closed', + 'mailbox', + 'mailbox_with_mail', + 'mailbox_with_no_mail', + 'package', + 'postal_horn', + 'inbox_tray', + 'outbox_tray', + 'scroll', + 'page_with_curl', + 'bookmark_tabs', + 'bar_chart', + 'chart_with_upwards_trend', + 'chart_with_downwards_trend', + 'page_facing_up', + 'date', + 'calendar', + 'calendar_spiral', + 'card_index', + 'card_box', + 'ballot_box', + 'file_cabinet', + 'clipboard', + 'notepad_spiral', + 'file_folder', + 'open_file_folder', + 'dividers', + 'newspaper2', + 'newspaper', + 'notebook', + 'closed_book', + 'green_book', + 'blue_book', + 'orange_book', + 'notebook_with_decorative_cover', + 'ledger', + 'books', + 'book', + 'link', + 'paperclip', + 'paperclips', + 'scissors', + 'triangular_ruler', + 'straight_ruler', + 'pushpin', + 'round_pushpin', + 'triangular_flag_on_post', + 'flag_white', + 'flag_black', + 'closed_lock_with_key', + 'lock', + 'unlock', + 'lock_with_ink_pen', + 'pen_ballpoint', + 'pen_fountain', + 'black_nib', + 'pencil', + 'pencil2', + 'crayon', + 'paintbrush', + 'mag', + 'mag_right' + ], + symbols: [ + '100', + '1234', + 'heart', + 'yellow_heart', + 'green_heart', + 'blue_heart', + 'purple_heart', + 'broken_heart', + 'heart_exclamation', + 'two_hearts', + 'revolving_hearts', + 'heartbeat', + 'heartpulse', + 'sparkling_heart', + 'cupid', + 'gift_heart', + 'heart_decoration', + 'peace', + 'cross', + 'star_and_crescent', + 'om_symbol', + 'wheel_of_dharma', + 'star_of_david', + 'six_pointed_star', + 'menorah', + 'yin_yang', + 'orthodox_cross', + 'place_of_worship', + 'ophiuchus', + 'aries', + 'taurus', + 'gemini', + 'cancer', + 'leo', + 'virgo', + 'libra', + 'scorpius', + 'sagittarius', + 'capricorn', + 'aquarius', + 'pisces', + 'id', + 'atom', + 'u7a7a', + 'u5272', + 'radioactive', + 'biohazard', + 'mobile_phone_off', + 'vibration_mode', + 'u6709', + 'u7121', + 'u7533', + 'u55b6', + 'u6708', + 'eight_pointed_black_star', + 'vs', + 'accept', + 'white_flower', + 'ideograph_advantage', + 'secret', + 'congratulations', + 'u5408', + 'u6e80', + 'u7981', + 'a', + 'b', + 'ab', + 'cl', + 'o2', + 'sos', + 'no_entry', + 'name_badge', + 'no_entry_sign', + 'x', + 'o', + 'anger', + 'hotsprings', + 'no_pedestrians', + 'do_not_litter', + 'no_bicycles', + 'non-potable_water', + 'underage', + 'no_mobile_phones', + 'exclamation', + 'grey_exclamation', + 'question', + 'grey_question', + 'bangbang', + 'interrobang', + 'low_brightness', + 'high_brightness', + 'trident', + 'fleur-de-lis', + 'part_alternation_mark', + 'warning', + 'children_crossing', + 'beginner', + 'recycle', + 'u6307', + 'chart', + 'sparkle', + 'eight_spoked_asterisk', + 'negative_squared_cross_mark', + 'white_check_mark', + 'diamond_shape_with_a_dot_inside', + 'cyclone', + 'loop', + 'globe_with_meridians', + 'm', + 'atm', + 'sa', + 'passport_control', + 'customs', + 'baggage_claim', + 'left_luggage', + 'wheelchair', + 'no_smoking', + 'wc', + 'parking', + 'potable_water', + 'mens', + 'womens', + 'baby_symbol', + 'restroom', + 'put_litter_in_its_place', + 'cinema', + 'signal_strength', + 'koko', + 'ng', + 'ok', + 'up', + 'cool', + 'new', + 'free', + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'keycap_ten', + 'arrow_forward', + 'pause_button', + 'play_pause', + 'stop_button', + 'record_button', + 'track_next', + 'track_previous', + 'fast_forward', + 'rewind', + 'twisted_rightwards_arrows', + 'repeat', + 'repeat_one', + 'arrow_backward', + 'arrow_up_small', + 'arrow_down_small', + 'arrow_double_up', + 'arrow_double_down', + 'arrow_right', + 'arrow_left', + 'arrow_up', + 'arrow_down', + 'arrow_upper_right', + 'arrow_lower_right', + 'arrow_lower_left', + 'arrow_upper_left', + 'arrow_up_down', + 'left_right_arrow', + 'arrows_counterclockwise', + 'arrow_right_hook', + 'leftwards_arrow_with_hook', + 'arrow_heading_up', + 'arrow_heading_down', + 'hash', + 'asterisk', + 'information_source', + 'abc', + 'abcd', + 'capital_abcd', + 'symbols', + 'musical_note', + 'notes', + 'wavy_dash', + 'curly_loop', + 'heavy_check_mark', + 'arrows_clockwise', + 'heavy_plus_sign', + 'heavy_minus_sign', + 'heavy_division_sign', + 'heavy_multiplication_x', + 'heavy_dollar_sign', + 'currency_exchange', + 'copyright', + 'registered', + 'tm', + 'end', + 'back', + 'on', + 'top', + 'soon', + 'ballot_box_with_check', + 'radio_button', + 'white_circle', + 'black_circle', + 'red_circle', + 'large_blue_circle', + 'small_orange_diamond', + 'small_blue_diamond', + 'large_orange_diamond', + 'large_blue_diamond', + 'small_red_triangle', + 'black_small_square', + 'white_small_square', + 'black_large_square', + 'white_large_square', + 'small_red_triangle_down', + 'black_medium_square', + 'white_medium_square', + 'black_medium_small_square', + 'white_medium_small_square', + 'black_square_button', + 'white_square_button', + 'speaker', + 'sound', + 'loud_sound', + 'mute', + 'mega', + 'loudspeaker', + 'bell', + 'no_bell', + 'black_joker', + 'mahjong', + 'spades', + 'clubs', + 'hearts', + 'diamonds', + 'flower_playing_cards', + 'thought_balloon', + 'anger_right', + 'speech_balloon', + 'clock1', + 'clock2', + 'clock3', + 'clock4', + 'clock5', + 'clock6', + 'clock7', + 'clock8', + 'clock9', + 'clock10', + 'clock11', + 'clock12', + 'clock130', + 'clock230', + 'clock330', + 'clock430', + 'clock530', + 'clock630', + 'clock730', + 'clock830', + 'clock930', + 'clock1030', + 'clock1130', + 'clock1230', + 'eye_in_speech_bubble', + 'speech_left', + 'eject', + 'black_heart', + 'octagonal_sign', + 'asterisk_symbol', + 'pound_symbol', + 'digit_nine', + 'digit_eight', + 'digit_seven', + 'digit_six', + 'digit_five', + 'digit_four', + 'digit_three', + 'digit_two', + 'digit_one', + 'digit_zero', + 'regional_indicator_z', + 'regional_indicator_y', + 'regional_indicator_x', + 'regional_indicator_w', + 'regional_indicator_v', + 'regional_indicator_u', + 'regional_indicator_t', + 'regional_indicator_s', + 'regional_indicator_r', + 'regional_indicator_q', + 'regional_indicator_p', + 'regional_indicator_o', + 'regional_indicator_n', + 'regional_indicator_m', + 'regional_indicator_l', + 'regional_indicator_k', + 'regional_indicator_j', + 'regional_indicator_i', + 'regional_indicator_h', + 'regional_indicator_g', + 'regional_indicator_f', + 'regional_indicator_e', + 'regional_indicator_d', + 'regional_indicator_c', + 'regional_indicator_b', + 'regional_indicator_a' + ], + flags: [ + 'flag_ac', + 'flag_af', + 'flag_al', + 'flag_dz', + 'flag_ad', + 'flag_ao', + 'flag_ai', + 'flag_ag', + 'flag_ar', + 'flag_am', + 'flag_aw', + 'flag_au', + 'flag_at', + 'flag_az', + 'flag_bs', + 'flag_bh', + 'flag_bd', + 'flag_bb', + 'flag_by', + 'flag_be', + 'flag_bz', + 'flag_bj', + 'flag_bm', + 'flag_bt', + 'flag_bo', + 'flag_ba', + 'flag_bw', + 'flag_br', + 'flag_bn', + 'flag_bg', + 'flag_bf', + 'flag_bi', + 'flag_cv', + 'flag_kh', + 'flag_cm', + 'flag_ca', + 'flag_ky', + 'flag_cf', + 'flag_td', + 'flag_cl', + 'flag_cn', + 'flag_co', + 'flag_km', + 'flag_cg', + 'flag_cd', + 'flag_cr', + 'flag_hr', + 'flag_cu', + 'flag_cy', + 'flag_cz', + 'flag_dk', + 'flag_dj', + 'flag_dm', + 'flag_do', + 'flag_ec', + 'flag_eg', + 'flag_sv', + 'flag_gq', + 'flag_er', + 'flag_ee', + 'flag_et', + 'flag_fk', + 'flag_fo', + 'flag_fj', + 'flag_fi', + 'flag_fr', + 'flag_pf', + 'flag_ga', + 'flag_gm', + 'flag_ge', + 'flag_de', + 'flag_gh', + 'flag_gi', + 'flag_gr', + 'flag_gl', + 'flag_gd', + 'flag_gu', + 'flag_gt', + 'flag_gn', + 'flag_gw', + 'flag_gy', + 'flag_ht', + 'flag_hn', + 'flag_hk', + 'flag_hu', + 'flag_is', + 'flag_in', + 'flag_id', + 'flag_ir', + 'flag_iq', + 'flag_ie', + 'flag_il', + 'flag_it', + 'flag_ci', + 'flag_jm', + 'flag_jp', + 'flag_je', + 'flag_jo', + 'flag_kz', + 'flag_ke', + 'flag_ki', + 'flag_xk', + 'flag_kw', + 'flag_kg', + 'flag_la', + 'flag_lv', + 'flag_lb', + 'flag_ls', + 'flag_lr', + 'flag_ly', + 'flag_li', + 'flag_lt', + 'flag_lu', + 'flag_mo', + 'flag_mk', + 'flag_mg', + 'flag_mw', + 'flag_my', + 'flag_mv', + 'flag_ml', + 'flag_mt', + 'flag_mh', + 'flag_mr', + 'flag_mu', + 'flag_mx', + 'flag_fm', + 'flag_md', + 'flag_mc', + 'flag_mn', + 'flag_me', + 'flag_ms', + 'flag_ma', + 'flag_mz', + 'flag_mm', + 'flag_na', + 'flag_nr', + 'flag_np', + 'flag_nl', + 'flag_nc', + 'flag_nz', + 'flag_ni', + 'flag_ne', + 'flag_ng', + 'flag_nu', + 'flag_kp', + 'flag_no', + 'flag_om', + 'flag_pk', + 'flag_pw', + 'flag_ps', + 'flag_pa', + 'flag_pg', + 'flag_py', + 'flag_pe', + 'flag_ph', + 'flag_pl', + 'flag_pt', + 'flag_pr', + 'flag_qa', + 'flag_ro', + 'flag_ru', + 'flag_rw', + 'flag_sh', + 'flag_kn', + 'flag_lc', + 'flag_vc', + 'flag_ws', + 'flag_sm', + 'flag_st', + 'flag_sa', + 'flag_sn', + 'flag_rs', + 'flag_sc', + 'flag_sl', + 'flag_sg', + 'flag_sk', + 'flag_si', + 'flag_sb', + 'flag_so', + 'flag_za', + 'flag_kr', + 'flag_es', + 'flag_lk', + 'flag_sd', + 'flag_sr', + 'flag_sz', + 'flag_se', + 'flag_ch', + 'flag_sy', + 'flag_tw', + 'flag_tj', + 'flag_tz', + 'flag_th', + 'flag_tl', + 'flag_tg', + 'flag_to', + 'flag_tt', + 'flag_tn', + 'flag_tr', + 'flag_tm', + 'flag_tv', + 'flag_ug', + 'flag_ua', + 'flag_ae', + 'flag_gb', + 'flag_us', + 'flag_vi', + 'flag_uy', + 'flag_uz', + 'flag_vu', + 'flag_va', + 'flag_ve', + 'flag_vn', + 'flag_wf', + 'flag_eh', + 'flag_ye', + 'flag_zm', + 'flag_zw', + 'flag_re', + 'flag_ax', + 'flag_ta', + 'flag_io', + 'flag_bq', + 'flag_cx', + 'flag_cc', + 'flag_gg', + 'flag_im', + 'flag_yt', + 'flag_nf', + 'flag_pn', + 'flag_bl', + 'flag_pm', + 'flag_gs', + 'flag_tk', + 'flag_bv', + 'flag_hm', + 'flag_sj', + 'flag_um', + 'flag_ic', + 'flag_ea', + 'flag_cp', + 'flag_dg', + 'flag_as', + 'flag_aq', + 'flag_vg', + 'flag_ck', + 'flag_cw', + 'flag_eu', + 'flag_gf', + 'flag_tf', + 'flag_gp', + 'flag_mq', + 'flag_mp', + 'flag_sx', + 'flag_ss', + 'flag_tc', + 'flag_mf' + ] +}; + +export const emojis = [ + 'grinning', + 'grimacing', + 'grin', + 'joy', + 'smiley', + 'smile', + 'sweat_smile', + 'laughing', + 'innocent', + 'wink', + 'blush', + 'slight_smile', + 'upside_down', + 'relaxed', + 'yum', + 'relieved', + 'heart_eyes', + 'kissing_heart', + 'kissing', + 'kissing_smiling_eyes', + 'kissing_closed_eyes', + 'stuck_out_tongue_winking_eye', + 'stuck_out_tongue_closed_eyes', + 'stuck_out_tongue', + 'money_mouth', + 'nerd', + 'sunglasses', + 'hugging', + 'smirk', + 'no_mouth', + 'neutral_face', + 'expressionless', + 'unamused', + 'rolling_eyes', + 'thinking', + 'flushed', + 'disappointed', + 'worried', + 'angry', + 'rage', + 'pensive', + 'confused', + 'slight_frown', + 'frowning2', + 'persevere', + 'confounded', + 'tired_face', + 'weary', + 'triumph', + 'open_mouth', + 'scream', + 'fearful', + 'cold_sweat', + 'hushed', + 'frowning', + 'anguished', + 'cry', + 'disappointed_relieved', + 'sleepy', + 'sweat', + 'sob', + 'dizzy_face', + 'astonished', + 'zipper_mouth', + 'mask', + 'thermometer_face', + 'head_bandage', + 'sleeping', + 'zzz', + 'poop', + 'smiling_imp', + 'imp', + 'japanese_ogre', + 'japanese_goblin', + 'skull', + 'ghost', + 'alien', + 'robot', + 'smiley_cat', + 'smile_cat', + 'joy_cat', + 'heart_eyes_cat', + 'smirk_cat', + 'kissing_cat', + 'scream_cat', + 'crying_cat_face', + 'pouting_cat', + 'raised_hands', + 'clap', + 'wave', + 'thumbsup', + 'thumbsdown', + 'punch', + 'fist', + 'v', + 'ok_hand', + 'raised_hand', + 'open_hands', + 'muscle', + 'pray', + 'point_up', + 'point_up_2', + 'point_down', + 'point_left', + 'point_right', + 'middle_finger', + 'hand_splayed', + 'metal', + 'vulcan', + 'writing_hand', + 'nail_care', + 'lips', + 'tongue', + 'ear', + 'nose', + 'eye', + 'eyes', + 'bust_in_silhouette', + 'busts_in_silhouette', + 'speaking_head', + 'baby', + 'boy', + 'girl', + 'man', + 'woman', + 'person_with_blond_hair', + 'older_man', + 'older_woman', + 'man_with_gua_pi_mao', + 'man_with_turban', + 'cop', + 'construction_worker', + 'guardsman', + 'spy', + 'santa', + 'angel', + 'princess', + 'bride_with_veil', + 'walking', + 'runner', + 'dancer', + 'dancers', + 'couple', + 'two_men_holding_hands', + 'two_women_holding_hands', + 'bow', + 'information_desk_person', + 'no_good', + 'ok_woman', + 'raising_hand', + 'person_with_pouting_face', + 'person_frowning', + 'haircut', + 'massage', + 'couple_with_heart', + 'couple_ww', + 'couple_mm', + 'couplekiss', + 'kiss_ww', + 'kiss_mm', + 'family', + 'family_mwg', + 'family_mwgb', + 'family_mwbb', + 'family_mwgg', + 'family_wwb', + 'family_wwg', + 'family_wwgb', + 'family_wwbb', + 'family_wwgg', + 'family_mmb', + 'family_mmg', + 'family_mmgb', + 'family_mmbb', + 'family_mmgg', + 'womans_clothes', + 'shirt', + 'jeans', + 'necktie', + 'dress', + 'bikini', + 'kimono', + 'lipstick', + 'kiss', + 'footprints', + 'high_heel', + 'sandal', + 'boot', + 'mans_shoe', + 'athletic_shoe', + 'womans_hat', + 'tophat', + 'helmet_with_cross', + 'mortar_board', + 'crown', + 'school_satchel', + 'pouch', + 'purse', + 'handbag', + 'briefcase', + 'eyeglasses', + 'dark_sunglasses', + 'ring', + 'closed_umbrella', + 'cowboy', + 'clown', + 'nauseated_face', + 'rofl', + 'drooling_face', + 'lying_face', + 'sneezing_face', + 'prince', + 'man_in_tuxedo', + 'mrs_claus', + 'face_palm', + 'shrug', + 'selfie', + 'man_dancing', + 'call_me', + 'raised_back_of_hand', + 'left_facing_fist', + 'right_facing_fist', + 'handshake', + 'fingers_crossed', + 'pregnant_woman', + 'dog', + 'cat', + 'mouse', + 'hamster', + 'rabbit', + 'bear', + 'panda_face', + 'koala', + 'tiger', + 'lion_face', + 'cow', + 'pig', + 'pig_nose', + 'frog', + 'octopus', + 'monkey_face', + 'see_no_evil', + 'hear_no_evil', + 'speak_no_evil', + 'monkey', + 'chicken', + 'penguin', + 'bird', + 'baby_chick', + 'hatching_chick', + 'hatched_chick', + 'wolf', + 'boar', + 'horse', + 'unicorn', + 'bee', + 'bug', + 'snail', + 'beetle', + 'ant', + 'spider', + 'scorpion', + 'crab', + 'snake', + 'turtle', + 'tropical_fish', + 'fish', + 'blowfish', + 'dolphin', + 'whale', + 'whale2', + 'crocodile', + 'leopard', + 'tiger2', + 'water_buffalo', + 'ox', + 'cow2', + 'dromedary_camel', + 'camel', + 'elephant', + 'goat', + 'ram', + 'sheep', + 'racehorse', + 'pig2', + 'rat', + 'mouse2', + 'rooster', + 'turkey', + 'dove', + 'dog2', + 'poodle', + 'cat2', + 'rabbit2', + 'chipmunk', + 'feet', + 'dragon', + 'dragon_face', + 'cactus', + 'christmas_tree', + 'evergreen_tree', + 'deciduous_tree', + 'palm_tree', + 'seedling', + 'herb', + 'shamrock', + 'four_leaf_clover', + 'bamboo', + 'tanabata_tree', + 'leaves', + 'fallen_leaf', + 'maple_leaf', + 'ear_of_rice', + 'hibiscus', + 'sunflower', + 'rose', + 'tulip', + 'blossom', + 'cherry_blossom', + 'bouquet', + 'mushroom', + 'chestnut', + 'jack_o_lantern', + 'shell', + 'spider_web', + 'earth_americas', + 'earth_africa', + 'earth_asia', + 'full_moon', + 'waning_gibbous_moon', + 'last_quarter_moon', + 'waning_crescent_moon', + 'new_moon', + 'waxing_crescent_moon', + 'first_quarter_moon', + 'waxing_gibbous_moon', + 'new_moon_with_face', + 'full_moon_with_face', + 'first_quarter_moon_with_face', + 'last_quarter_moon_with_face', + 'sun_with_face', + 'crescent_moon', + 'star', + 'star2', + 'dizzy', + 'sparkles', + 'comet', + 'sunny', + 'white_sun_small_cloud', + 'partly_sunny', + 'white_sun_cloud', + 'white_sun_rain_cloud', + 'cloud', + 'cloud_rain', + 'thunder_cloud_rain', + 'cloud_lightning', + 'zap', + 'fire', + 'boom', + 'snowflake', + 'cloud_snow', + 'snowman2', + 'snowman', + 'wind_blowing_face', + 'dash', + 'cloud_tornado', + 'fog', + 'umbrella2', + 'umbrella', + 'droplet', + 'sweat_drops', + 'ocean', + 'eagle', + 'duck', + 'bat', + 'shark', + 'owl', + 'fox', + 'butterfly', + 'deer', + 'gorilla', + 'lizard', + 'rhino', + 'wilted_rose', + 'shrimp', + 'squid', + 'green_apple', + 'apple', + 'pear', + 'tangerine', + 'lemon', + 'banana', + 'watermelon', + 'grapes', + 'strawberry', + 'melon', + 'cherries', + 'peach', + 'pineapple', + 'tomato', + 'eggplant', + 'hot_pepper', + 'corn', + 'sweet_potato', + 'honey_pot', + 'bread', + 'cheese', + 'poultry_leg', + 'meat_on_bone', + 'fried_shrimp', + 'cooking', + 'hamburger', + 'fries', + 'hotdog', + 'pizza', + 'spaghetti', + 'taco', + 'burrito', + 'ramen', + 'stew', + 'fish_cake', + 'sushi', + 'bento', + 'curry', + 'rice_ball', + 'rice', + 'rice_cracker', + 'oden', + 'dango', + 'shaved_ice', + 'ice_cream', + 'icecream', + 'cake', + 'birthday', + 'custard', + 'candy', + 'lollipop', + 'chocolate_bar', + 'popcorn', + 'doughnut', + 'cookie', + 'beer', + 'beers', + 'wine_glass', + 'cocktail', + 'tropical_drink', + 'champagne', + 'sake', + 'tea', + 'coffee', + 'baby_bottle', + 'fork_and_knife', + 'fork_knife_plate', + 'croissant', + 'avocado', + 'cucumber', + 'bacon', + 'potato', + 'carrot', + 'french_bread', + 'salad', + 'shallow_pan_of_food', + 'stuffed_flatbread', + 'champagne_glass', + 'tumbler_glass', + 'spoon', + 'egg', + 'milk', + 'peanuts', + 'kiwi', + 'pancakes', + 'soccer', + 'basketball', + 'football', + 'baseball', + 'tennis', + 'volleyball', + 'rugby_football', + '8ball', + 'golf', + 'golfer', + 'ping_pong', + 'badminton', + 'hockey', + 'field_hockey', + 'cricket', + 'ski', + 'skier', + 'snowboarder', + 'ice_skate', + 'bow_and_arrow', + 'fishing_pole_and_fish', + 'rowboat', + 'swimmer', + 'surfer', + 'bath', + 'basketball_player', + 'lifter', + 'bicyclist', + 'mountain_bicyclist', + 'horse_racing', + 'levitate', + 'trophy', + 'running_shirt_with_sash', + 'medal', + 'military_medal', + 'reminder_ribbon', + 'rosette', + 'ticket', + 'tickets', + 'performing_arts', + 'art', + 'circus_tent', + 'microphone', + 'headphones', + 'musical_score', + 'musical_keyboard', + 'saxophone', + 'trumpet', + 'guitar', + 'violin', + 'clapper', + 'video_game', + 'space_invader', + 'dart', + 'game_die', + 'slot_machine', + 'bowling', + 'cartwheel', + 'juggling', + 'wrestlers', + 'boxing_glove', + 'martial_arts_uniform', + 'water_polo', + 'handball', + 'goal', + 'fencer', + 'first_place', + 'second_place', + 'third_place', + 'drum', + 'red_car', + 'taxi', + 'blue_car', + 'bus', + 'trolleybus', + 'race_car', + 'police_car', + 'ambulance', + 'fire_engine', + 'minibus', + 'truck', + 'articulated_lorry', + 'tractor', + 'motorcycle', + 'bike', + 'rotating_light', + 'oncoming_police_car', + 'oncoming_bus', + 'oncoming_automobile', + 'oncoming_taxi', + 'aerial_tramway', + 'mountain_cableway', + 'suspension_railway', + 'railway_car', + 'train', + 'monorail', + 'bullettrain_side', + 'bullettrain_front', + 'light_rail', + 'mountain_railway', + 'steam_locomotive', + 'train2', + 'metro', + 'tram', + 'station', + 'helicopter', + 'airplane_small', + 'airplane', + 'airplane_departure', + 'airplane_arriving', + 'sailboat', + 'motorboat', + 'speedboat', + 'ferry', + 'cruise_ship', + 'rocket', + 'satellite_orbital', + 'seat', + 'anchor', + 'construction', + 'fuelpump', + 'busstop', + 'vertical_traffic_light', + 'traffic_light', + 'checkered_flag', + 'ship', + 'ferris_wheel', + 'roller_coaster', + 'carousel_horse', + 'construction_site', + 'foggy', + 'tokyo_tower', + 'factory', + 'fountain', + 'rice_scene', + 'mountain', + 'mountain_snow', + 'mount_fuji', + 'volcano', + 'japan', + 'camping', + 'tent', + 'park', + 'motorway', + 'railway_track', + 'sunrise', + 'sunrise_over_mountains', + 'desert', + 'beach', + 'island', + 'city_sunset', + 'city_dusk', + 'cityscape', + 'night_with_stars', + 'bridge_at_night', + 'milky_way', + 'stars', + 'sparkler', + 'fireworks', + 'rainbow', + 'homes', + 'european_castle', + 'japanese_castle', + 'stadium', + 'statue_of_liberty', + 'house', + 'house_with_garden', + 'house_abandoned', + 'office', + 'department_store', + 'post_office', + 'european_post_office', + 'hospital', + 'bank', + 'hotel', + 'convenience_store', + 'school', + 'love_hotel', + 'wedding', + 'classical_building', + 'church', + 'mosque', + 'synagogue', + 'kaaba', + 'shinto_shrine', + 'shopping_cart', + 'scooter', + 'motor_scooter', + 'canoe', + 'watch', + 'iphone', + 'calling', + 'computer', + 'keyboard', + 'desktop', + 'printer', + 'mouse_three_button', + 'trackball', + 'joystick', + 'compression', + 'minidisc', + 'floppy_disk', + 'cd', + 'dvd', + 'vhs', + 'camera', + 'camera_with_flash', + 'video_camera', + 'movie_camera', + 'projector', + 'film_frames', + 'telephone_receiver', + 'telephone', + 'pager', + 'fax', + 'tv', + 'radio', + 'microphone2', + 'level_slider', + 'control_knobs', + 'stopwatch', + 'timer', + 'alarm_clock', + 'clock', + 'hourglass_flowing_sand', + 'hourglass', + 'satellite', + 'battery', + 'electric_plug', + 'bulb', + 'flashlight', + 'candle', + 'wastebasket', + 'oil', + 'money_with_wings', + 'dollar', + 'yen', + 'euro', + 'pound', + 'moneybag', + 'credit_card', + 'gem', + 'scales', + 'wrench', + 'hammer', + 'hammer_pick', + 'tools', + 'pick', + 'nut_and_bolt', + 'gear', + 'chains', + 'gun', + 'bomb', + 'knife', + 'dagger', + 'crossed_swords', + 'shield', + 'smoking', + 'skull_crossbones', + 'coffin', + 'urn', + 'amphora', + 'crystal_ball', + 'prayer_beads', + 'barber', + 'alembic', + 'telescope', + 'microscope', + 'hole', + 'pill', + 'syringe', + 'thermometer', + 'label', + 'bookmark', + 'toilet', + 'shower', + 'bathtub', + 'key', + 'key2', + 'couch', + 'sleeping_accommodation', + 'bed', + 'door', + 'bellhop', + 'frame_photo', + 'map', + 'beach_umbrella', + 'moyai', + 'shopping_bags', + 'balloon', + 'flags', + 'ribbon', + 'gift', + 'confetti_ball', + 'tada', + 'dolls', + 'wind_chime', + 'crossed_flags', + 'izakaya_lantern', + 'envelope', + 'envelope_with_arrow', + 'incoming_envelope', + 'e-mail', + 'love_letter', + 'postbox', + 'mailbox_closed', + 'mailbox', + 'mailbox_with_mail', + 'mailbox_with_no_mail', + 'package', + 'postal_horn', + 'inbox_tray', + 'outbox_tray', + 'scroll', + 'page_with_curl', + 'bookmark_tabs', + 'bar_chart', + 'chart_with_upwards_trend', + 'chart_with_downwards_trend', + 'page_facing_up', + 'date', + 'calendar', + 'calendar_spiral', + 'card_index', + 'card_box', + 'ballot_box', + 'file_cabinet', + 'clipboard', + 'notepad_spiral', + 'file_folder', + 'open_file_folder', + 'dividers', + 'newspaper2', + 'newspaper', + 'notebook', + 'closed_book', + 'green_book', + 'blue_book', + 'orange_book', + 'notebook_with_decorative_cover', + 'ledger', + 'books', + 'book', + 'link', + 'paperclip', + 'paperclips', + 'scissors', + 'triangular_ruler', + 'straight_ruler', + 'pushpin', + 'round_pushpin', + 'triangular_flag_on_post', + 'flag_white', + 'flag_black', + 'closed_lock_with_key', + 'lock', + 'unlock', + 'lock_with_ink_pen', + 'pen_ballpoint', + 'pen_fountain', + 'black_nib', + 'pencil', + 'pencil2', + 'crayon', + 'paintbrush', + 'mag', + 'mag_right', + '100', + '1234', + 'heart', + 'yellow_heart', + 'green_heart', + 'blue_heart', + 'purple_heart', + 'broken_heart', + 'heart_exclamation', + 'two_hearts', + 'revolving_hearts', + 'heartbeat', + 'heartpulse', + 'sparkling_heart', + 'cupid', + 'gift_heart', + 'heart_decoration', + 'peace', + 'cross', + 'star_and_crescent', + 'om_symbol', + 'wheel_of_dharma', + 'star_of_david', + 'six_pointed_star', + 'menorah', + 'yin_yang', + 'orthodox_cross', + 'place_of_worship', + 'ophiuchus', + 'aries', + 'taurus', + 'gemini', + 'cancer', + 'leo', + 'virgo', + 'libra', + 'scorpius', + 'sagittarius', + 'capricorn', + 'aquarius', + 'pisces', + 'id', + 'atom', + 'u7a7a', + 'u5272', + 'radioactive', + 'biohazard', + 'mobile_phone_off', + 'vibration_mode', + 'u6709', + 'u7121', + 'u7533', + 'u55b6', + 'u6708', + 'eight_pointed_black_star', + 'vs', + 'accept', + 'white_flower', + 'ideograph_advantage', + 'secret', + 'congratulations', + 'u5408', + 'u6e80', + 'u7981', + 'a', + 'b', + 'ab', + 'cl', + 'o2', + 'sos', + 'no_entry', + 'name_badge', + 'no_entry_sign', + 'x', + 'o', + 'anger', + 'hotsprings', + 'no_pedestrians', + 'do_not_litter', + 'no_bicycles', + 'non-potable_water', + 'underage', + 'no_mobile_phones', + 'exclamation', + 'grey_exclamation', + 'question', + 'grey_question', + 'bangbang', + 'interrobang', + 'low_brightness', + 'high_brightness', + 'trident', + 'fleur-de-lis', + 'part_alternation_mark', + 'warning', + 'children_crossing', + 'beginner', + 'recycle', + 'u6307', + 'chart', + 'sparkle', + 'eight_spoked_asterisk', + 'negative_squared_cross_mark', + 'white_check_mark', + 'diamond_shape_with_a_dot_inside', + 'cyclone', + 'loop', + 'globe_with_meridians', + 'm', + 'atm', + 'sa', + 'passport_control', + 'customs', + 'baggage_claim', + 'left_luggage', + 'wheelchair', + 'no_smoking', + 'wc', + 'parking', + 'potable_water', + 'mens', + 'womens', + 'baby_symbol', + 'restroom', + 'put_litter_in_its_place', + 'cinema', + 'signal_strength', + 'koko', + 'ng', + 'ok', + 'up', + 'cool', + 'new', + 'free', + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'keycap_ten', + 'arrow_forward', + 'pause_button', + 'play_pause', + 'stop_button', + 'record_button', + 'track_next', + 'track_previous', + 'fast_forward', + 'rewind', + 'twisted_rightwards_arrows', + 'repeat', + 'repeat_one', + 'arrow_backward', + 'arrow_up_small', + 'arrow_down_small', + 'arrow_double_up', + 'arrow_double_down', + 'arrow_right', + 'arrow_left', + 'arrow_up', + 'arrow_down', + 'arrow_upper_right', + 'arrow_lower_right', + 'arrow_lower_left', + 'arrow_upper_left', + 'arrow_up_down', + 'left_right_arrow', + 'arrows_counterclockwise', + 'arrow_right_hook', + 'leftwards_arrow_with_hook', + 'arrow_heading_up', + 'arrow_heading_down', + 'hash', + 'asterisk', + 'information_source', + 'abc', + 'abcd', + 'capital_abcd', + 'symbols', + 'musical_note', + 'notes', + 'wavy_dash', + 'curly_loop', + 'heavy_check_mark', + 'arrows_clockwise', + 'heavy_plus_sign', + 'heavy_minus_sign', + 'heavy_division_sign', + 'heavy_multiplication_x', + 'heavy_dollar_sign', + 'currency_exchange', + 'copyright', + 'registered', + 'tm', + 'end', + 'back', + 'on', + 'top', + 'soon', + 'ballot_box_with_check', + 'radio_button', + 'white_circle', + 'black_circle', + 'red_circle', + 'large_blue_circle', + 'small_orange_diamond', + 'small_blue_diamond', + 'large_orange_diamond', + 'large_blue_diamond', + 'small_red_triangle', + 'black_small_square', + 'white_small_square', + 'black_large_square', + 'white_large_square', + 'small_red_triangle_down', + 'black_medium_square', + 'white_medium_square', + 'black_medium_small_square', + 'white_medium_small_square', + 'black_square_button', + 'white_square_button', + 'speaker', + 'sound', + 'loud_sound', + 'mute', + 'mega', + 'loudspeaker', + 'bell', + 'no_bell', + 'black_joker', + 'mahjong', + 'spades', + 'clubs', + 'hearts', + 'diamonds', + 'flower_playing_cards', + 'thought_balloon', + 'anger_right', + 'speech_balloon', + 'clock1', + 'clock2', + 'clock3', + 'clock4', + 'clock5', + 'clock6', + 'clock7', + 'clock8', + 'clock9', + 'clock10', + 'clock11', + 'clock12', + 'clock130', + 'clock230', + 'clock330', + 'clock430', + 'clock530', + 'clock630', + 'clock730', + 'clock830', + 'clock930', + 'clock1030', + 'clock1130', + 'clock1230', + 'eye_in_speech_bubble', + 'speech_left', + 'eject', + 'black_heart', + 'octagonal_sign', + 'asterisk_symbol', + 'pound_symbol', + 'digit_nine', + 'digit_eight', + 'digit_seven', + 'digit_six', + 'digit_five', + 'digit_four', + 'digit_three', + 'digit_two', + 'digit_one', + 'digit_zero', + 'regional_indicator_z', + 'regional_indicator_y', + 'regional_indicator_x', + 'regional_indicator_w', + 'regional_indicator_v', + 'regional_indicator_u', + 'regional_indicator_t', + 'regional_indicator_s', + 'regional_indicator_r', + 'regional_indicator_q', + 'regional_indicator_p', + 'regional_indicator_o', + 'regional_indicator_n', + 'regional_indicator_m', + 'regional_indicator_l', + 'regional_indicator_k', + 'regional_indicator_j', + 'regional_indicator_i', + 'regional_indicator_h', + 'regional_indicator_g', + 'regional_indicator_f', + 'regional_indicator_e', + 'regional_indicator_d', + 'regional_indicator_c', + 'regional_indicator_b', + 'regional_indicator_a', + 'flag_ac', + 'flag_af', + 'flag_al', + 'flag_dz', + 'flag_ad', + 'flag_ao', + 'flag_ai', + 'flag_ag', + 'flag_ar', + 'flag_am', + 'flag_aw', + 'flag_au', + 'flag_at', + 'flag_az', + 'flag_bs', + 'flag_bh', + 'flag_bd', + 'flag_bb', + 'flag_by', + 'flag_be', + 'flag_bz', + 'flag_bj', + 'flag_bm', + 'flag_bt', + 'flag_bo', + 'flag_ba', + 'flag_bw', + 'flag_br', + 'flag_bn', + 'flag_bg', + 'flag_bf', + 'flag_bi', + 'flag_cv', + 'flag_kh', + 'flag_cm', + 'flag_ca', + 'flag_ky', + 'flag_cf', + 'flag_td', + 'flag_cl', + 'flag_cn', + 'flag_co', + 'flag_km', + 'flag_cg', + 'flag_cd', + 'flag_cr', + 'flag_hr', + 'flag_cu', + 'flag_cy', + 'flag_cz', + 'flag_dk', + 'flag_dj', + 'flag_dm', + 'flag_do', + 'flag_ec', + 'flag_eg', + 'flag_sv', + 'flag_gq', + 'flag_er', + 'flag_ee', + 'flag_et', + 'flag_fk', + 'flag_fo', + 'flag_fj', + 'flag_fi', + 'flag_fr', + 'flag_pf', + 'flag_ga', + 'flag_gm', + 'flag_ge', + 'flag_de', + 'flag_gh', + 'flag_gi', + 'flag_gr', + 'flag_gl', + 'flag_gd', + 'flag_gu', + 'flag_gt', + 'flag_gn', + 'flag_gw', + 'flag_gy', + 'flag_ht', + 'flag_hn', + 'flag_hk', + 'flag_hu', + 'flag_is', + 'flag_in', + 'flag_id', + 'flag_ir', + 'flag_iq', + 'flag_ie', + 'flag_il', + 'flag_it', + 'flag_ci', + 'flag_jm', + 'flag_jp', + 'flag_je', + 'flag_jo', + 'flag_kz', + 'flag_ke', + 'flag_ki', + 'flag_xk', + 'flag_kw', + 'flag_kg', + 'flag_la', + 'flag_lv', + 'flag_lb', + 'flag_ls', + 'flag_lr', + 'flag_ly', + 'flag_li', + 'flag_lt', + 'flag_lu', + 'flag_mo', + 'flag_mk', + 'flag_mg', + 'flag_mw', + 'flag_my', + 'flag_mv', + 'flag_ml', + 'flag_mt', + 'flag_mh', + 'flag_mr', + 'flag_mu', + 'flag_mx', + 'flag_fm', + 'flag_md', + 'flag_mc', + 'flag_mn', + 'flag_me', + 'flag_ms', + 'flag_ma', + 'flag_mz', + 'flag_mm', + 'flag_na', + 'flag_nr', + 'flag_np', + 'flag_nl', + 'flag_nc', + 'flag_nz', + 'flag_ni', + 'flag_ne', + 'flag_ng', + 'flag_nu', + 'flag_kp', + 'flag_no', + 'flag_om', + 'flag_pk', + 'flag_pw', + 'flag_ps', + 'flag_pa', + 'flag_pg', + 'flag_py', + 'flag_pe', + 'flag_ph', + 'flag_pl', + 'flag_pt', + 'flag_pr', + 'flag_qa', + 'flag_ro', + 'flag_ru', + 'flag_rw', + 'flag_sh', + 'flag_kn', + 'flag_lc', + 'flag_vc', + 'flag_ws', + 'flag_sm', + 'flag_st', + 'flag_sa', + 'flag_sn', + 'flag_rs', + 'flag_sc', + 'flag_sl', + 'flag_sg', + 'flag_sk', + 'flag_si', + 'flag_sb', + 'flag_so', + 'flag_za', + 'flag_kr', + 'flag_es', + 'flag_lk', + 'flag_sd', + 'flag_sr', + 'flag_sz', + 'flag_se', + 'flag_ch', + 'flag_sy', + 'flag_tw', + 'flag_tj', + 'flag_tz', + 'flag_th', + 'flag_tl', + 'flag_tg', + 'flag_to', + 'flag_tt', + 'flag_tn', + 'flag_tr', + 'flag_tm', + 'flag_tv', + 'flag_ug', + 'flag_ua', + 'flag_ae', + 'flag_gb', + 'flag_us', + 'flag_vi', + 'flag_uy', + 'flag_uz', + 'flag_vu', + 'flag_va', + 'flag_ve', + 'flag_vn', + 'flag_wf', + 'flag_eh', + 'flag_ye', + 'flag_zm', + 'flag_zw', + 'flag_re', + 'flag_ax', + 'flag_ta', + 'flag_io', + 'flag_bq', + 'flag_cx', + 'flag_cc', + 'flag_gg', + 'flag_im', + 'flag_yt', + 'flag_nf', + 'flag_pn', + 'flag_bl', + 'flag_pm', + 'flag_gs', + 'flag_tk', + 'flag_bv', + 'flag_hm', + 'flag_sj', + 'flag_um', + 'flag_ic', + 'flag_ea', + 'flag_cp', + 'flag_dg', + 'flag_as', + 'flag_aq', + 'flag_vg', + 'flag_ck', + 'flag_cw', + 'flag_eu', + 'flag_gf', + 'flag_tf', + 'flag_gp', + 'flag_mq', + 'flag_mp', + 'flag_sx', + 'flag_ss', + 'flag_tc', + 'flag_mf' +]; diff --git a/app/lib/realm.js b/app/lib/realm.js index 17d30e26a..147660455 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -142,6 +142,21 @@ const url = { } }; +const messagesReactionsUsernamesSchema = { + name: 'messagesReactionsUsernames', + properties: { + value: 'string' + } +}; + +const messagesReactionsSchema = { + name: 'messagesReactions', + primaryKey: 'emoji', + properties: { + emoji: 'string', + usernames: { type: 'list', objectType: 'messagesReactionsUsernames' } + } +}; const messagesEditedBySchema = { name: 'messagesEditedBy', @@ -173,7 +188,8 @@ const messagesSchema = { status: { type: 'int', optional: true }, pinned: { type: 'bool', optional: true }, starred: { type: 'bool', optional: true }, - editedBy: 'messagesEditedBy' + editedBy: 'messagesEditedBy', + reactions: { type: 'list', objectType: 'messagesReactions' } } }; @@ -222,7 +238,9 @@ const schema = [ url, frequentlyUsedEmojiSchema, customEmojiAliasesSchema, - customEmojisSchema + customEmojisSchema, + messagesReactionsSchema, + messagesReactionsUsernamesSchema ]; class DB { databases = { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index dc0bf1bd6..f56eb1c90 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,6 +1,7 @@ import Random from 'react-native-meteor/lib/Random'; import { AsyncStorage, Platform } from 'react-native'; import { hashPassword } from 'react-native-meteor/lib/utils'; +import _ from 'lodash'; import RNFetchBlob from 'react-native-fetch-blob'; import reduxStore from './createStore'; @@ -264,6 +265,8 @@ const RocketChat = { // loadHistory returns message.starred as object // stream-room-messages returns message.starred as an array message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); + message.reactions = _.map(message.reactions, (value, key) => + ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); return message; }, loadMessagesForRoom(rid, end, cb) { @@ -586,6 +589,9 @@ const RocketChat = { }, setUserPresenceDefaultStatus(status) { return call('UserPresence:setDefaultStatus', status); + }, + setReaction(emoji, messageId) { + return call('setReaction', emoji, messageId); } }; diff --git a/app/reducers/messages.js b/app/reducers/messages.js index 7c4022375..ae17f395e 100644 --- a/app/reducers/messages.js +++ b/app/reducers/messages.js @@ -8,7 +8,8 @@ const initialState = { editing: false, permalink: '', showActions: false, - showErrorActions: false + showErrorActions: false, + showReactionPicker: false }; export default function messages(state = initialState, action) { @@ -96,6 +97,12 @@ export default function messages(state = initialState, action) { ...state, message: {} }; + case types.MESSAGES.TOGGLE_REACTION_PICKER: + return { + ...state, + showReactionPicker: !state.showReactionPicker, + actionMessage: action.message + }; default: return state; } diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js index 3eacad9cb..9c89b46f5 100644 --- a/app/views/RoomView/ListView.js +++ b/app/views/RoomView/ListView.js @@ -4,10 +4,16 @@ import cloneReferencedElement from 'react-clone-referenced-element'; import { ScrollView, ListView as OldList2 } from 'react-native'; import moment from 'moment'; import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + import DateSeparator from './DateSeparator'; import UnreadSeparator from './UnreadSeparator'; +import styles from './styles'; +import debounce from '../../utils/debounce'; +import Typing from '../../containers/Typing'; +import database from '../../lib/realm'; -const DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; +const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100; export class DataSource extends OldList.DataSource { getRowData(sectionIndex: number, rowIndex: number): any { @@ -20,9 +26,58 @@ export class DataSource extends OldList.DataSource { } } +const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); + @connect(state => ({ lastOpen: state.room.lastOpen })) + +export class List extends React.Component { + static propTypes = { + onEndReached: PropTypes.func, + renderFooter: PropTypes.func, + renderRow: PropTypes.func, + room: PropTypes.string, + end: PropTypes.bool + }; + constructor(props) { + super(props); + this.data = database + .objects('messages') + .filtered('rid = $0', props.room) + .sorted('ts', true); + this.dataSource = ds.cloneWithRows(this.data); + } + componentDidMount() { + this.data.addListener(this.updateState); + } + shouldComponentUpdate(nextProps) { + return this.props.end !== nextProps.end; + } + updateState = debounce(() => { + // this.setState({ + this.dataSource = this.dataSource.cloneWithRows(this.data); + this.forceUpdate(); + // }); + }, 100); + + render() { + return ( } + onEndReached={() => this.props.onEndReached(this.data)} + dataSource={this.dataSource} + renderRow={item => this.props.renderRow(item)} + initialListSize={10} + keyboardShouldPersistTaps='always' + keyboardDismissMode='none' + />); + } +} + export class ListView extends OldList2 { constructor(props) { super(props); diff --git a/app/views/RoomView/ReactionPicker.js b/app/views/RoomView/ReactionPicker.js new file mode 100644 index 000000000..96ff5fd2d --- /dev/null +++ b/app/views/RoomView/ReactionPicker.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, Platform } from 'react-native'; +import { connect } from 'react-redux'; +import Modal from 'react-native-modal'; +import { responsive } from 'react-native-responsive-ui'; +import EmojiPicker from '../../containers/EmojiPicker'; +import { toggleReactionPicker } from '../../actions/messages'; +import styles from './styles'; + +const margin = Platform.OS === 'android' ? 40 : 20; +const tabEmojiStyle = { fontSize: 15 }; + +@connect(state => ({ + showReactionPicker: state.messages.showReactionPicker +}), dispatch => ({ + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) +})) +@responsive +export default class extends React.Component { + static propTypes = { + window: PropTypes.any, + showReactionPicker: PropTypes.bool, + toggleReactionPicker: PropTypes.func, + onEmojiSelected: PropTypes.func + }; + + shouldComponentUpdate(nextProps) { + return nextProps.showReactionPicker !== this.props.showReactionPicker || this.props.window.width !== nextProps.window.width; + } + + onEmojiSelected(emoji, shortname) { + // standard emojis: `emoji` is unicode and `shortname` is :joy: + // custom emojis: only `emoji` is returned with shortname type (:joy:) + // to set reactions, we need shortname type + this.props.onEmojiSelected(shortname || emoji); + } + + render() { + const { width, height } = this.props.window; + return ( + this.props.toggleReactionPicker()} + onBackButtonPress={() => this.props.toggleReactionPicker()} + animationIn='fadeIn' + animationOut='fadeOut' + > + + this.onEmojiSelected(emoji, shortname)} + /> + + + ); + } +} diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 94bbd4f12..dd76d5d43 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -1,44 +1,45 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, Button, SafeAreaView, Platform } from 'react-native'; +import { Text, View, Button, SafeAreaView, Platform, Keyboard } from 'react-native'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import equal from 'deep-equal'; import KeyboardSpacer from 'react-native-keyboard-spacer'; -import { ListView } from './ListView'; +import { List } from './ListView'; import * as actions from '../../actions'; import { openRoom, setLastOpen } from '../../actions/room'; -import { editCancel } from '../../actions/messages'; +import { editCancel, toggleReactionPicker } from '../../actions/messages'; +import { setKeyboardOpen, setKeyboardClosed } from '../../actions/keyboard'; import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; import MessageActions from '../../containers/MessageActions'; import MessageErrorActions from '../../containers/MessageErrorActions'; import MessageBox from '../../containers/MessageBox'; -import Typing from '../../containers/Typing'; + import Header from '../../containers/Header'; import RoomsHeader from './Header'; +import ReactionPicker from './ReactionPicker'; import Banner from './banner'; import styles from './styles'; -import debounce from '../../utils/debounce'; - -const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); - -const typing = () => ; @connect( state => ({ Site_Url: state.settings.Site_Url || state.server ? state.server.server : '', Message_TimeFormat: state.settings.Message_TimeFormat, loading: state.messages.isFetching, - user: state.login.user + user: state.login.user, + actionMessage: state.messages.actionMessage }), dispatch => ({ actions: bindActionCreators(actions, dispatch), openRoom: room => dispatch(openRoom(room)), editCancel: () => dispatch(editCancel()), - setLastOpen: date => dispatch(setLastOpen(date)) + setLastOpen: date => dispatch(setLastOpen(date)), + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)), + setKeyboardOpen: () => dispatch(setKeyboardOpen()), + setKeyboardClosed: () => dispatch(setKeyboardClosed()) }) ) export default class RoomView extends React.Component { @@ -51,7 +52,12 @@ export default class RoomView extends React.Component { rid: PropTypes.string, name: PropTypes.string, Site_Url: PropTypes.string, - Message_TimeFormat: PropTypes.string + Message_TimeFormat: PropTypes.string, + loading: PropTypes.bool, + actionMessage: PropTypes.object, + toggleReactionPicker: PropTypes.func.isRequired, + setKeyboardOpen: PropTypes.func, + setKeyboardClosed: PropTypes.func }; static navigationOptions = ({ navigation }) => ({ @@ -63,22 +69,17 @@ export default class RoomView extends React.Component { this.rid = props.rid || props.navigation.state.params.room.rid; - this.name = this.props.name || - this.props.navigation.state.params.name || - this.props.navigation.state.params.room.name; + this.name = props.name || + props.navigation.state.params.name || + props.navigation.state.params.room.name; this.opened = new Date(); - this.data = database - .objects('messages') - .filtered('rid = $0', this.rid) - .sorted('ts', true); - const rowIds = this.data.map((row, index) => index); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.state = { - dataSource: ds.cloneWithRows(this.data, rowIds), loaded: true, joined: typeof props.rid === 'undefined', - readOnly: false + room: {} }; + this.onReactionPress = this.onReactionPress.bind(this); } componentWillMount() { @@ -86,59 +87,58 @@ export default class RoomView extends React.Component { title: this.name }); this.updateRoom(); - this.props.openRoom({ rid: this.rid, name: this.name, ls: this.room.ls }); - if (this.room.alert || this.room.unread || this.room.userMentions) { - this.props.setLastOpen(this.room.ls); + this.props.openRoom({ rid: this.rid, name: this.name, ls: this.state.room.ls }); + if (this.state.room.alert || this.state.room.unread || this.state.room.userMentions) { + this.props.setLastOpen(this.state.room.ls); } else { this.props.setLastOpen(null); } - this.data.addListener(this.updateState); + this.rooms.addListener(this.updateRoom); + this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => this.props.setKeyboardOpen()); + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => this.props.setKeyboardClosed()); } shouldComponentUpdate(nextProps, nextState) { return !(equal(this.props, nextProps) && equal(this.state, nextState)); } componentWillUnmount() { clearTimeout(this.timer); - this.data.removeAllListeners(); + this.rooms.removeAllListeners(); + this.keyboardDidShowListener.remove(); + this.keyboardDidHideListener.remove(); this.props.editCancel(); } - onEndReached = () => { - if ( - // rowCount && - this.state.loaded && - this.state.loadingMore !== true && - this.state.end !== true - ) { - this.setState({ - loadingMore: true - }); - requestAnimationFrame(() => { - const lastRowData = this.data[this.data.length - 1]; - if (!lastRowData) { - return; - } - RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => { - this.setState({ - loadingMore: false, - end - }); - }); - }); + onEndReached = (data) => { + if (this.props.loading || this.state.end) { + return; } + if (!this.state.loaded) { + alert(2); + return; + } + + requestAnimationFrame(() => { + const lastRowData = data[data.length - 1]; + if (!lastRowData) { + return; + } + RocketChat.loadMessagesForRoom(this.rid, lastRowData.ts, ({ end }) => end && this.setState({ + end + })); + }); } - updateState = debounce(() => { - const rowIds = this.data.map((row, index) => index); - this.setState({ - dataSource: this.state.dataSource.cloneWithRows(this.data, rowIds) - }); - }, 50); + onReactionPress = (shortname, messageId) => { + if (!messageId) { + RocketChat.setReaction(shortname, this.props.actionMessage._id); + return this.props.toggleReactionPicker(); + } + RocketChat.setReaction(shortname, messageId); + }; updateRoom = () => { - [this.room] = this.rooms; - this.setState({ readOnly: this.room.ro }); + this.setState({ room: this.rooms[0] }); } sendMessage = message => RocketChat.sendMessage(this.rid, message).then(() => { @@ -156,10 +156,12 @@ export default class RoomView extends React.Component { ); @@ -174,7 +176,7 @@ export default class RoomView extends React.Component { ); } - if (this.state.readOnly) { + if (this.state.room.ro) { return ( This room is read only @@ -185,36 +187,28 @@ export default class RoomView extends React.Component { }; renderHeader = () => { - if (this.state.loadingMore) { - return Loading more messages...; - } - if (this.state.end) { return Start of conversation; } + return Loading more messages...; } render() { return ( - this.renderItem(item)} - initialListSize={10} - keyboardShouldPersistTaps='always' - keyboardDismissMode='none' /> {this.renderFooter()} - + {this.state.room._id ? : null} + {Platform.OS === 'ios' ? : null} ); diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js index 2e2713d05..6b68d2333 100644 --- a/app/views/RoomView/styles.js +++ b/app/views/RoomView/styles.js @@ -1,4 +1,4 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, Platform } from 'react-native'; export default StyleSheet.create({ typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 }, @@ -33,5 +33,13 @@ export default StyleSheet.create({ }, readOnly: { padding: 10 + }, + reactionPickerContainer: { + // width: width - 20, + // height: width - 20, + paddingHorizontal: Platform.OS === 'android' ? 11 : 10, + backgroundColor: '#F7F7F7', + borderRadius: 4, + flexDirection: 'column' } }); diff --git a/package-lock.json b/package-lock.json index df02c28ad..fb9a3f7ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1481,6 +1481,16 @@ "babel-helper-is-void-0": "0.2.0" } }, + "babel-plugin-module-resolver": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-2.7.1.tgz", + "integrity": "sha1-GL48Qt31n3pFbJ4FEs2ROU9uS+E=", + "requires": { + "find-babel-config": "1.1.0", + "glob": "7.1.2", + "resolve": "1.5.0" + } + }, "babel-plugin-react-transform": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-transform/-/babel-plugin-react-transform-3.0.0.tgz", @@ -2172,6 +2182,18 @@ "semver": "5.4.1" } }, + "babel-preset-expo": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-4.0.0.tgz", + "integrity": "sha512-EWFC6WJzZX5t2zZfLNdJXUkNMusUkxP5V+GrXaSk8pKbWGjE3TD2i33ncpF/4aQM9QGDm+SH6pImZJOqIDlRUw==", + "requires": { + "babel-plugin-module-resolver": "2.7.1", + "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-export-extensions": "6.22.0", + "babel-preset-react-native": "4.0.0" + } + }, "babel-preset-fbjs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-2.1.4.tgz", @@ -4610,11 +4632,6 @@ "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-1.1.1.tgz", "integrity": "sha512-vkcJJZEb7JXDY883Nx1Lkmb6noM3j1SfSt8L9tVFhZPnPQiFq+Nkd5evc77+tRVS4ChTUSr34voThsglI/ja/A==" }, - "emoji-datasource": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/emoji-datasource/-/emoji-datasource-4.0.3.tgz", - "integrity": "sha1-1gDnDwVoMnyyjPp79B1T88SGeQE=" - }, "emoji-regex": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", @@ -5771,6 +5788,15 @@ } } }, + "find-babel-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.1.0.tgz", + "integrity": "sha1-rMAQQ6Z0n+w0Qpvmtk9ULrtdY1U=", + "requires": { + "json5": "0.5.1", + "path-exists": "3.0.0" + } + }, "find-cache-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", @@ -12610,6 +12636,14 @@ "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.1.tgz", "integrity": "sha1-DiPbMC0Du0o/KNwHLcryqaEXjtg=" }, + "react-native-responsive-ui": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-native-responsive-ui/-/react-native-responsive-ui-1.1.1.tgz", + "integrity": "sha1-60GDnU85Uf8CVmAYXDapqc4zdZ8=", + "requires": { + "lodash": "4.17.4" + } + }, "react-native-scrollable-tab-view": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/react-native-scrollable-tab-view/-/react-native-scrollable-tab-view-0.8.0.tgz", @@ -14445,11 +14479,6 @@ "strip-ansi": "4.0.0" } }, - "string.fromcodepoint": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz", - "integrity": "sha1-jZeDM8C8klOPUPOD5IiPPlYZ1lM=" - }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", diff --git a/package.json b/package.json index 71bf39267..7dd888867 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-remove-console": "^6.8.5", "babel-polyfill": "^6.26.0", + "babel-preset-expo": "^4.0.0", "deep-equal": "^1.0.1", "ejson": "^2.1.2", - "emoji-datasource": "^4.0.3", "lodash": "^4.17.4", "moment": "^2.20.1", "prop-types": "^15.6.0", @@ -51,6 +51,7 @@ "react-native-modal": "^4.1.1", "react-native-optimized-flatlist": "^1.0.3", "react-native-push-notification": "^3.0.1", + "react-native-responsive-ui": "^1.1.1", "react-native-scrollable-tab-view": "^0.8.0", "react-native-slider": "^0.11.0", "react-native-splash-screen": "^3.0.6", @@ -72,7 +73,6 @@ "remote-redux-devtools": "^0.5.12", "simple-markdown": "^0.3.1", "snyk": "^1.61.1", - "string.fromcodepoint": "^0.2.1", "strip-ansi": "^4.0.0" }, "devDependencies": { From 1d749eea6491d7f3c9e8eb7a48e0442ad9b63628 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 30 Jan 2018 17:49:22 -0200 Subject: [PATCH 06/11] Fix TestFlight upload (#212) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9541943c0..cea7b0274 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -203,7 +203,7 @@ jobs: name: Fastlane Tesflight Upload command: | cd ios - fastlane pilot upload --changelog "$(sh ../.circleci/changelog.sh)" + fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)" workflows: version: 2 From a3b173c9c1a6176d40a354db315a94188bd2d3d7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 30 Jan 2018 18:59:38 -0200 Subject: [PATCH 07/11] Reactions (#223) * * Tracking emoji * Fixed users/rooms regex tracking * fix * Autocomplete emoji * fix * Emoji picker refactored * * Scroll edge color effect * layout fix * Fix * Open reaction picker modal * tmp * tmp * Reaction size * Fixed URL image warning * * Show reactions of messages * Messages with a reaction should show a button to add more reactions * Set reaction * Toggle reaction * 'User have reacted' style * Show who have reacted onLongPress * Style * Vibration onLongPress * Close reaction picker on Android's back button press * Close emoji when keyboard is open * lint * Android layout fix * performance [1] * fix lint * who reacted fix * :x * test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7dd888867..e6bc7f8dc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "android": "react-native run-android", "storybook": "storybook start -p 7007", "snyk-protect": "snyk protect", - "prepare": "npm run snyk-protect" + "prepare": "exit 0" }, "rnpm": { "assets": [ From 9f96136571460f76ff0fecdb06d2fbb0ef5349d4 Mon Sep 17 00:00:00 2001 From: Saket Kumar <15ucs114@lnmiit.ac.in> Date: Thu, 1 Feb 2018 18:18:58 +0530 Subject: [PATCH 08/11] Adds share TEXT option in message's context menu (#220) * Adds share text option in message's context menu --- app/containers/MessageActions.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js index c7ed5d2c4..e362c69d8 100644 --- a/app/containers/MessageActions.js +++ b/app/containers/MessageActions.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Alert, Clipboard, Vibration } from 'react-native'; +import { Alert, Clipboard, Vibration, Share } from 'react-native'; import { connect } from 'react-redux'; import ActionSheet from 'react-native-actionsheet'; import * as moment from 'moment'; @@ -107,6 +107,9 @@ export default class MessageActions extends React.Component { // Copy this.options.push('Copy Message'); this.COPY_INDEX = this.options.length - 1; + // Share + this.options.push('Share Message'); + this.SHARE_INDEX = this.options.length - 1; // Quote if (!this.isRoomReadOnly()) { this.options.push('Quote'); @@ -260,6 +263,12 @@ export default class MessageActions extends React.Component { showToast('Copied to clipboard!'); } + handleShare = async() => { + Share.share({ + message: this.props.actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '') + }); + }; + handleStar() { this.props.toggleStarRequest(this.props.actionMessage); } @@ -301,6 +310,9 @@ export default class MessageActions extends React.Component { case this.COPY_INDEX: this.handleCopy(); break; + case this.SHARE_INDEX: + this.handleShare(); + break; case this.QUOTE_INDEX: this.handleQuote(); break; From b1e423b7a596354f866ecf0a858e9e350e4a6d42 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 6 Feb 2018 15:57:03 -0200 Subject: [PATCH 09/11] Fix push for iOS --- app/push.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/push.js b/app/push.js index 6c0687d8b..fa135d155 100644 --- a/app/push.js +++ b/app/push.js @@ -4,10 +4,10 @@ import EJSON from 'ejson'; import { goRoom } from './containers/routes/NavigationService'; const handleNotification = (notification) => { - if (notification.usernInteraction) { + if (!notification.userInteraction) { return; } - const { rid, name } = EJSON.parse(notification.ejson); + const { rid, name } = EJSON.parse(notification.ejson || notification.data.ejson); return rid && name && goRoom({ rid, name }); }; PushNotification.configure({ From fe61d3428b87a0caecbb3f9bc94b337fcfbf3a9f Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 6 Feb 2018 15:57:03 -0200 Subject: [PATCH 10/11] Fix push for iOS --- app/push.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/push.js b/app/push.js index 6c0687d8b..2a8de1ff6 100644 --- a/app/push.js +++ b/app/push.js @@ -4,11 +4,13 @@ import EJSON from 'ejson'; import { goRoom } from './containers/routes/NavigationService'; const handleNotification = (notification) => { - if (notification.usernInteraction) { + if (!notification.userInteraction) { return; } - const { rid, name } = EJSON.parse(notification.ejson); - return rid && name && goRoom({ rid, name }); + const { + rid, name, sender, type + } = EJSON.parse(notification.ejson || notification.data.ejson); + return rid && goRoom({ rid, name: type === 'd' ? sender.username : name }); }; PushNotification.configure({ From ed5a1386e007d8799f59413188ce7e2c675c04f0 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 8 Feb 2018 12:08:50 -0200 Subject: [PATCH 11/11] Improvements on emoji picker / message box (#227) * Emoji keyboard * Keyboard emoji working * animation and flatlist * fix * Unread separator animation * easeInEaseOut animation --- android/app/build.gradle | 1 + .../com/rocketchatrn/MainApplication.java | 22 ++-- android/settings.gradle | 2 + app/actions/actionsTypes.js | 14 ++- app/actions/keyboard.js | 13 --- app/actions/room.js | 6 + app/animations/collapse.js | 3 +- app/animations/fade.js | 3 +- app/containers/Avatar.js | 25 ++-- app/containers/EmojiPicker/CustomEmoji.js | 5 +- app/containers/EmojiPicker/EmojiCategory.js | 38 +++--- app/containers/EmojiPicker/TabBar.js | 2 +- app/containers/EmojiPicker/index.js | 14 ++- app/containers/Header.js | 2 +- .../MessageBox/AnimatedContainer.js | 74 ------------ app/containers/MessageBox/EmojiKeyboard.js | 23 ++++ app/containers/MessageBox/index.js | 108 +++++++++--------- .../MessageBox/{style.js => styles.js} | 11 +- app/containers/MessageErrorActions.js | 2 +- app/containers/Sidebar.js | 5 - app/containers/message/Emoji.js | 6 +- app/containers/message/Image.js | 2 +- app/containers/message/Markdown.js | 3 +- app/containers/message/PhotoModal.js | 2 +- app/containers/message/ReactionsModal.js | 2 +- app/containers/message/index.js | 43 ++----- app/containers/message/styles.js | 7 ++ app/presentation/KeyboardView.js | 12 +- app/reducers/index.js | 4 +- app/reducers/keyboard.js | 22 ---- app/reducers/room.js | 8 +- app/views/Photo.js | 2 +- app/views/RoomView/Header/index.js | 2 +- app/views/RoomView/ListView.js | 16 +-- app/views/RoomView/ReactionPicker.js | 2 +- app/views/RoomView/UnreadSeparator.js | 23 ++-- app/views/RoomView/index.js | 56 ++++----- app/views/RoomView/styles.js | 4 +- app/views/RoomsListView/Header/index.js | 2 +- app/views/SelectUsersView.js | 2 +- index.android.js | 2 + ios/RocketChatRN.xcodeproj/project.pbxproj | 60 ++++++++++ package-lock.json | 24 ++-- package.json | 5 +- 44 files changed, 336 insertions(+), 348 deletions(-) delete mode 100644 app/actions/keyboard.js delete mode 100644 app/containers/MessageBox/AnimatedContainer.js create mode 100644 app/containers/MessageBox/EmojiKeyboard.js rename app/containers/MessageBox/{style.js => styles.js} (91%) delete mode 100644 app/reducers/keyboard.js diff --git a/android/app/build.gradle b/android/app/build.gradle index 1d5816d81..e6c65008c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -144,6 +144,7 @@ android { } dependencies { + compile project(":reactnativekeyboardinput") compile project(':react-native-splash-screen') compile project(':react-native-video') compile project(':react-native-push-notification') diff --git a/android/app/src/main/java/com/rocketchatrn/MainApplication.java b/android/app/src/main/java/com/rocketchatrn/MainApplication.java index 72ed5b81b..beb8cc02c 100644 --- a/android/app/src/main/java/com/rocketchatrn/MainApplication.java +++ b/android/app/src/main/java/com/rocketchatrn/MainApplication.java @@ -16,6 +16,7 @@ import com.facebook.soloader.SoLoader; import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; import com.brentvatne.react.ReactVideoPackage; import com.remobile.toast.RCTToastPackage; +import com.wix.reactnativekeyboardinput.KeyboardInputPackage; import java.util.Arrays; import java.util.List; @@ -33,16 +34,17 @@ public class MainApplication extends Application implements ReactApplication { protected List getPackages() { return Arrays.asList( new MainReactPackage(), - new SvgPackage(), - new ImagePickerPackage(), - new VectorIconsPackage(), - new RNFetchBlobPackage(), - new ZeroconfReactPackage(), - new RealmReactPackage(), - new ReactNativePushNotificationPackage(), - new ReactVideoPackage(), - new SplashScreenReactPackage(), - new RCTToastPackage() + new SvgPackage(), + new ImagePickerPackage(), + new VectorIconsPackage(), + new RNFetchBlobPackage(), + new ZeroconfReactPackage(), + new RealmReactPackage(), + new ReactNativePushNotificationPackage(), + new ReactVideoPackage(), + new SplashScreenReactPackage(), + new RCTToastPackage(), + new KeyboardInputPackage(MainApplication.this) ); } }; diff --git a/android/settings.gradle b/android/settings.gradle index 5455aadb5..236de741b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'RocketChatRN' +include ':reactnativekeyboardinput' +project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android') include ':react-native-splash-screen' project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android') include ':react-native-video' diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 4e67c7478..2422a207a 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -28,7 +28,17 @@ export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [ ]); export const USER = createRequestTypes('USER', ['SET']); export const ROOMS = createRequestTypes('ROOMS', [...defaultTypes, 'SET_SEARCH']); -export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'SOMEONE_TYPING', 'OPEN', 'CLOSE', 'USER_TYPING', 'MESSAGE_RECEIVED', 'SET_LAST_OPEN']); +export const ROOM = createRequestTypes('ROOM', [ + 'ADD_USER_TYPING', + 'REMOVE_USER_TYPING', + 'SOMEONE_TYPING', + 'OPEN', + 'CLOSE', + 'USER_TYPING', + 'MESSAGE_RECEIVED', + 'SET_LAST_OPEN', + 'LAYOUT_ANIMATION' +]); export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const MESSAGES = createRequestTypes('MESSAGES', [ ...defaultTypes, @@ -82,4 +92,4 @@ export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST' export const INCREMENT = 'INCREMENT'; export const DECREMENT = 'DECREMENT'; -export const KEYBOARD = createRequestTypes('KEYBOARD', ['OPEN', 'CLOSE']); + diff --git a/app/actions/keyboard.js b/app/actions/keyboard.js deleted file mode 100644 index 6c598d024..000000000 --- a/app/actions/keyboard.js +++ /dev/null @@ -1,13 +0,0 @@ -import * as types from './actionsTypes'; - -export function setKeyboardOpen() { - return { - type: types.KEYBOARD.OPEN - }; -} - -export function setKeyboardClosed() { - return { - type: types.KEYBOARD.CLOSE - }; -} diff --git a/app/actions/room.js b/app/actions/room.js index ebae2bd6c..1b3a67e1f 100644 --- a/app/actions/room.js +++ b/app/actions/room.js @@ -55,3 +55,9 @@ export function setLastOpen(date = new Date()) { date }; } + +export function layoutAnimation() { + return { + type: types.ROOM.LAYOUT_ANIMATION + }; +} diff --git a/app/animations/collapse.js b/app/animations/collapse.js index 35bca751d..05cfce52a 100644 --- a/app/animations/collapse.js +++ b/app/animations/collapse.js @@ -42,7 +42,8 @@ export default class Panel extends React.Component { this.state.animation, { toValue: finalValue, - duration: 150 + duration: 150, + useNativeDriver: true } ).start(); } diff --git a/app/animations/fade.js b/app/animations/fade.js index b9f276903..36da8da33 100644 --- a/app/animations/fade.js +++ b/app/animations/fade.js @@ -29,7 +29,8 @@ export default class Fade extends React.Component { } Animated.timing(this._visibility, { toValue: nextProps.visible ? 1 : 0, - duration: 300 + duration: 300, + useNativeDriver: true }).start(() => { this.setState({ visible: nextProps.visible }); }); diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index 8ff05e6d0..f547b7788 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, View, ViewPropTypes } from 'react-native'; import { CachedImage } from 'react-native-img-cache'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import avatarInitialsAndColor from '../utils/avatarInitialsAndColor'; @@ -23,8 +23,16 @@ const styles = StyleSheet.create({ @connect(state => ({ baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' })) - -class Avatar extends React.PureComponent { +export default class Avatar extends React.PureComponent { + static propTypes = { + style: ViewPropTypes.style, + baseUrl: PropTypes.string, + text: PropTypes.string.isRequired, + avatar: PropTypes.string, + size: PropTypes.number, + borderRadius: PropTypes.number, + type: PropTypes.string + }; render() { const { text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd' @@ -76,14 +84,3 @@ class Avatar extends React.PureComponent { ); } } - -Avatar.propTypes = { - style: PropTypes.object, - baseUrl: PropTypes.string, - text: PropTypes.string.isRequired, - avatar: PropTypes.string, - size: PropTypes.number, - borderRadius: PropTypes.number, - type: PropTypes.string -}; -export default Avatar; diff --git a/app/containers/EmojiPicker/CustomEmoji.js b/app/containers/EmojiPicker/CustomEmoji.js index 5988296f3..8215f0ea6 100644 --- a/app/containers/EmojiPicker/CustomEmoji.js +++ b/app/containers/EmojiPicker/CustomEmoji.js @@ -1,4 +1,5 @@ import React from 'react'; +import { ViewPropTypes } from 'react-native'; import PropTypes from 'prop-types'; import { CachedImage } from 'react-native-img-cache'; import { connect } from 'react-redux'; @@ -6,11 +7,11 @@ import { connect } from 'react-redux'; @connect(state => ({ baseUrl: state.settings.Site_Url })) -export default class extends React.Component { +export default class CustomEmoji extends React.Component { static propTypes = { baseUrl: PropTypes.string.isRequired, emoji: PropTypes.object.isRequired, - style: PropTypes.object + style: ViewPropTypes.style } shouldComponentUpdate() { return false; diff --git a/app/containers/EmojiPicker/EmojiCategory.js b/app/containers/EmojiPicker/EmojiCategory.js index 0fc3c65d7..09e15fde3 100644 --- a/app/containers/EmojiPicker/EmojiCategory.js +++ b/app/containers/EmojiPicker/EmojiCategory.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, TouchableOpacity, Platform } from 'react-native'; +import { Text, TouchableOpacity, Platform } from 'react-native'; import { emojify } from 'react-emojione'; import { responsive } from 'react-native-responsive-ui'; +import { OptimizedFlatList } from 'react-native-optimized-flatlist'; import styles from './styles'; import CustomEmoji from './CustomEmoji'; - +import scrollPersistTaps from '../../utils/scrollPersistTaps'; const emojisPerRow = Platform.OS === 'ios' ? 8 : 9; @@ -21,8 +22,6 @@ const renderEmoji = (emoji, size) => { }; -const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve)); - @responsive export default class EmojiCategory extends React.Component { static propTypes = { @@ -40,23 +39,7 @@ export default class EmojiCategory extends React.Component { this.emojis = []; } componentWillMount() { - this.emojis = this.props.emojis.slice(0, emojisPerRow * 3).map(item => this.renderItem(item, this.size)); - } - async componentDidMount() { - const array = this.props.emojis; - const temparray = []; - let i; - let j; - const chunk = emojisPerRow * 3; - for (i = chunk, j = array.length; i < j; i += chunk) { - temparray.push(array.slice(i, i + chunk)); - } - temparray.forEach(async(items) => { - await nextFrame(); - this.emojis = this.emojis.concat(items.map(item => this.renderItem(item, this.size))); - this.forceUpdate(); - await nextFrame(); - }); + this.emojis = this.props.emojis; } shouldComponentUpdate() { @@ -75,6 +58,17 @@ export default class EmojiCategory extends React.Component { } render() { - return {this.emojis}; + return ( + (item.isCustom && item.content) || item} + data={this.props.emojis} + renderItem={({ item }) => this.renderItem(item, this.size)} + numColumns={emojisPerRow} + initialNumToRender={45} + getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })} + removeClippedSubviews + {...scrollPersistTaps} + /> + ); } } diff --git a/app/containers/EmojiPicker/TabBar.js b/app/containers/EmojiPicker/TabBar.js index fb23b30ea..4886006a6 100644 --- a/app/containers/EmojiPicker/TabBar.js +++ b/app/containers/EmojiPicker/TabBar.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { View, TouchableOpacity, Text } from 'react-native'; import styles from './styles'; -export default class extends React.PureComponent { +export default class TabBar extends React.PureComponent { static propTypes = { goToPage: PropTypes.func, activeTab: PropTypes.number, diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 4209b5cc8..c722da0ed 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -8,15 +8,15 @@ import TabBar from './TabBar'; import EmojiCategory from './EmojiCategory'; import styles from './styles'; import categories from './categories'; -import scrollPersistTaps from '../../utils/scrollPersistTaps'; import database from '../../lib/realm'; import { emojisByCategory } from '../../emojis'; const scrollProps = { - keyboardShouldPersistTaps: 'always' + keyboardShouldPersistTaps: 'always', + keyboardDismissMode: 'none' }; -export default class extends Component { +export default class EmojiPicker extends Component { static propTypes = { onEmojiSelected: PropTypes.func, tabEmojiStyle: PropTypes.object, @@ -45,9 +45,10 @@ export default class extends Component { this.customEmojis.addListener(this.updateCustomEmojis); this.updateFrequentlyUsed(); this.updateCustomEmojis(); - setTimeout(() => this.setState({ show: true }), 100); } - + componentDidMount() { + requestAnimationFrame(() => this.setState({ show: true })); + } componentWillUnmount() { this.frequentlyUsed.removeAllListeners(); this.customEmojis.removeAllListeners(); @@ -122,13 +123,14 @@ export default class extends Component { } contentProps={scrollProps} + // prerenderingSiblingsNumber={1} > { categories.tabs.map((tab, i) => ( {this.renderCategory(tab.category, i)} diff --git a/app/containers/Header.js b/app/containers/Header.js index b285d4121..7dfa46f1b 100644 --- a/app/containers/Header.js +++ b/app/containers/Header.js @@ -33,7 +33,7 @@ const styles = StyleSheet.create({ } }); -export default class extends React.PureComponent { +export default class Header extends React.PureComponent { static propTypes = { subview: PropTypes.object.isRequired } diff --git a/app/containers/MessageBox/AnimatedContainer.js b/app/containers/MessageBox/AnimatedContainer.js deleted file mode 100644 index 688ccd591..000000000 --- a/app/containers/MessageBox/AnimatedContainer.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Animated } from 'react-native'; - -export default class MessageBox extends React.PureComponent { - static propTypes = { - subview: PropTypes.object.isRequired, - visible: PropTypes.bool.isRequired, - messageboxHeight: PropTypes.number.isRequired - } - - constructor(props) { - super(props); - this.animatedBottom = new Animated.Value(0); - this.state = { render: false }; - } - - componentWillReceiveProps(nextProps) { - if (this.props.visible === nextProps.visible) { - return; - } - if (nextProps.visible) { - return this.show(); - } - this.hide(); - } - - show() { - this.setState({ render: true }); - this.animatedBottom.setValue(0); - Animated.timing(this.animatedBottom, { - toValue: 1, - duration: 300, - useNativeDriver: true - }).start(); - } - - hide() { - Animated.timing(this.animatedBottom, { - toValue: 0, - duration: 300, - useNativeDriver: true - }).start(); - setTimeout(() => { - this.setState({ render: false }); - }, 300); - } - - render() { - const bottom = this.animatedBottom.interpolate({ - inputRange: [0, 1], - outputRange: [0, -this.props.messageboxHeight - 200] - }); - - if (!this.state.render) { - return null; - } - - return ( - - {this.props.subview} - - ); - } -} diff --git a/app/containers/MessageBox/EmojiKeyboard.js b/app/containers/MessageBox/EmojiKeyboard.js new file mode 100644 index 000000000..9a6cb8b91 --- /dev/null +++ b/app/containers/MessageBox/EmojiKeyboard.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { View } from 'react-native'; +import { KeyboardRegistry } from 'react-native-keyboard-input'; +import { Provider } from 'react-redux'; +import store from '../../lib/createStore'; +import EmojiPicker from '../EmojiPicker'; +import styles from './styles'; + +export default class EmojiKeyboard extends React.PureComponent { + onEmojiSelected = (emoji) => { + KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji }); + } + render() { + return ( + + + this.onEmojiSelected(emoji)} /> + + + ); + } +} +KeyboardRegistry.registerKeyboard('EmojiKeyboard', () => EmojiKeyboard); diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index b5f8aa3f2..db4a0a061 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -1,22 +1,21 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity, Keyboard, StyleSheet } from 'react-native'; +import { View, TextInput, SafeAreaView, FlatList, Text, TouchableOpacity } from 'react-native'; import Icon from 'react-native-vector-icons/MaterialIcons'; import ImagePicker from 'react-native-image-picker'; import { connect } from 'react-redux'; import { emojify } from 'react-emojione'; -import { userTyping } from '../../actions/room'; +import { KeyboardAccessoryView } from 'react-native-keyboard-input'; +import { userTyping, layoutAnimation } from '../../actions/room'; import RocketChat from '../../lib/rocketchat'; import { editRequest, editCancel, clearInput } from '../../actions/messages'; -import styles from './style'; +import styles from './styles'; import MyIcon from '../icons'; import database from '../../lib/realm'; import Avatar from '../Avatar'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; -import AnimatedContainer from './AnimatedContainer'; -import EmojiPicker from '../EmojiPicker'; -import scrollPersistTaps from '../../utils/scrollPersistTaps'; import { emojis } from '../../emojis'; +import './EmojiKeyboard'; const MENTIONS_TRACKING_TYPE_USERS = '@'; const MENTIONS_TRACKING_TYPE_EMOJIS = ':'; @@ -29,13 +28,13 @@ const onlyUnique = function onlyUnique(value, index, self) { room: state.room, message: state.messages.message, editing: state.messages.editing, - baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', - isKeyboardOpen: state.keyboard.isOpen + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' }), dispatch => ({ editCancel: () => dispatch(editCancel()), editRequest: message => dispatch(editRequest(message)), typing: status => dispatch(userTyping(status)), - clearInput: () => dispatch(clearInput()) + clearInput: () => dispatch(clearInput()), + layoutAnimation: () => dispatch(layoutAnimation()) })) export default class MessageBox extends React.PureComponent { static propTypes = { @@ -48,23 +47,23 @@ export default class MessageBox extends React.PureComponent { editing: PropTypes.bool, typing: PropTypes.func, clearInput: PropTypes.func, - isKeyboardOpen: PropTypes.bool + layoutAnimation: PropTypes.func } constructor(props) { super(props); this.state = { - messageboxHeight: 0, text: '', mentions: [], showMentionsContainer: false, - showEmojiContainer: false, + showEmojiKeyboard: false, trackingType: '' }; this.users = []; this.rooms = []; this.emojis = []; this.customEmojis = []; + this._onEmojiSelected = this._onEmojiSelected.bind(this); } componentWillReceiveProps(nextProps) { if (this.props.message !== nextProps.message && nextProps.message.msg) { @@ -72,8 +71,6 @@ export default class MessageBox extends React.PureComponent { this.component.focus(); } else if (!nextProps.message) { this.setState({ text: '' }); - } else if (this.props.isKeyboardOpen !== nextProps.isKeyboardOpen && nextProps.isKeyboardOpen) { - this.closeEmoji(); } } @@ -100,6 +97,10 @@ export default class MessageBox extends React.PureComponent { }); } + onKeyboardResigned() { + this.closeEmoji(); + } + get leftButtons() { const { editing } = this.props; if (editing) { @@ -111,7 +112,7 @@ export default class MessageBox extends React.PureComponent { onPress={() => this.editCancel()} />); } - return !this.state.showEmojiContainer ? ( this.openEmoji()} accessibilityLabel='Open emoji selector' @@ -184,13 +185,11 @@ export default class MessageBox extends React.PureComponent { } async openEmoji() { await this.setState({ - showEmojiContainer: true, - showMentionsContainer: false + showEmojiKeyboard: true }); - Keyboard.dismiss(); } closeEmoji() { - this.setState({ showEmojiContainer: false }); + this.setState({ showEmojiKeyboard: false }); } submit(message) { this.setState({ text: '' }); @@ -324,9 +323,12 @@ export default class MessageBox extends React.PureComponent { } identifyMentionKeyword(keyword, type) { + if (!this.state.showMentionsContainer) { + this.props.layoutAnimation(); + } this.setState({ showMentionsContainer: true, - showEmojiContainer: false, + showEmojiKeyboard: false, trackingType: type }); this.updateMentions(keyword, type); @@ -360,8 +362,9 @@ export default class MessageBox extends React.PureComponent { this.component.focus(); requestAnimationFrame(() => this.stopTrackingMention()); } - _onEmojiSelected(emoji) { + _onEmojiSelected(keyboardId, params) { const { text } = this.state; + const { emoji } = params; let newText = ''; // if messagebox has an active cursor @@ -390,7 +393,7 @@ export default class MessageBox extends React.PureComponent { return ( @@ -399,7 +402,7 @@ export default class MessageBox extends React.PureComponent { return ( {emojify(`:${ item }:`, { output: 'unicode' })} @@ -433,34 +436,24 @@ export default class MessageBox extends React.PureComponent { ); } - renderEmoji() { - const emojiContainer = ( - - this._onEmojiSelected(emoji)} /> - - ); - const { showEmojiContainer, messageboxHeight } = this.state; - return ; - } - renderMentions() { - const list = ( - this.renderMentionItem(item)} - keyExtractor={item => item._id || item} - {...scrollPersistTaps} - /> - ); - const { showMentionsContainer, messageboxHeight } = this.state; - return ; - } - render() { + renderMentions = () => ( + this.renderMentionItem(item)} + keyExtractor={item => item._id || item} + keyboardShouldPersistTaps='always' + /> + ); + + renderContent() { return ( - + [ + this.renderMentions(), this.setState({ messageboxHeight: event.nativeEvent.layout.height })} > {this.leftButtons} @@ -480,9 +473,22 @@ export default class MessageBox extends React.PureComponent { {this.rightButtons} - {this.renderMentions()} - {this.renderEmoji()} - + ] + ); + } + + render() { + return ( + this.renderContent()} + kbInputRef={this.component} + kbComponent={this.state.showEmojiKeyboard ? 'EmojiKeyboard' : null} + onKeyboardResigned={() => this.onKeyboardResigned()} + onItemSelected={this._onEmojiSelected} + trackInteractive + revealKeyboardInteractive + requiresSameParentToManageScrollView + /> ); } } diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/styles.js similarity index 91% rename from app/containers/MessageBox/style.js rename to app/containers/MessageBox/styles.js index 98395ce4f..fed138d6b 100644 --- a/app/containers/MessageBox/style.js +++ b/app/containers/MessageBox/styles.js @@ -21,13 +21,14 @@ export default StyleSheet.create({ flexGrow: 0 }, textBoxInput: { - paddingHorizontal: 5, textAlignVertical: 'center', maxHeight: 120, flexGrow: 1, width: 1, paddingTop: 15, - paddingBottom: 15 + paddingBottom: 15, + paddingLeft: 0, + paddingRight: 0 }, editing: { backgroundColor: '#fff5df' @@ -37,6 +38,7 @@ export default StyleSheet.create({ fontSize: 20, textAlign: 'center', padding: 15, + paddingHorizontal: 21, flex: 0 }, actionRow: { @@ -98,5 +100,10 @@ export default StyleSheet.create({ fontWeight: 'bold', textAlign: 'center', width: 46 + }, + emojiKeyboardContainer: { + flex: 1, + borderTopColor: '#ECECEC', + borderTopWidth: 1 } }); diff --git a/app/containers/MessageErrorActions.js b/app/containers/MessageErrorActions.js index b96ca79aa..4f3439fae 100644 --- a/app/containers/MessageErrorActions.js +++ b/app/containers/MessageErrorActions.js @@ -16,7 +16,7 @@ import database from '../lib/realm'; errorActionsHide: () => dispatch(errorActionsHide()) }) ) -export default class MessageActions extends React.Component { +export default class MessageErrorActions extends React.Component { static propTypes = { errorActionsHide: PropTypes.func.isRequired, showErrorActions: PropTypes.bool.isRequired, diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index c93f5857b..ae76794f8 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ScrollView, Text, View, StyleSheet, FlatList, TouchableHighlight } from 'react-native'; -import { DrawerItems } from 'react-navigation'; import { connect } from 'react-redux'; import database from '../lib/realm'; @@ -101,10 +100,6 @@ export default class Sidebar extends Component { return ( - { const emojiExtension = customEmojis[content]; if (emojiExtension) { const emoji = { extension: emojiExtension, content }; - const style = StyleSheet.flatten(styles.customEmoji); element.props.children = ( - + ); } return element; diff --git a/app/containers/message/PhotoModal.js b/app/containers/message/PhotoModal.js index 694015b01..7d378794e 100644 --- a/app/containers/message/PhotoModal.js +++ b/app/containers/message/PhotoModal.js @@ -25,7 +25,7 @@ const styles = { } }; -export default class extends React.PureComponent { +export default class PhotoModal extends React.PureComponent { static propTypes = { title: PropTypes.string.isRequired, image: PropTypes.string.isRequired, diff --git a/app/containers/message/ReactionsModal.js b/app/containers/message/ReactionsModal.js index c62dab65c..08491518f 100644 --- a/app/containers/message/ReactionsModal.js +++ b/app/containers/message/ReactionsModal.js @@ -52,7 +52,7 @@ const styles = StyleSheet.create({ }); const standardEmojiStyle = { fontSize: 20 }; const customEmojiStyle = { width: 20, height: 20 }; -export default class extends React.PureComponent { +export default class ReactionsModal extends React.PureComponent { static propTypes = { isVisible: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 840cfa0b2..a361ef57c 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TouchableHighlight, Text, TouchableOpacity, Animated, Keyboard, StyleSheet, Vibration } from 'react-native'; +import { View, TouchableHighlight, Text, TouchableOpacity, Vibration } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; import equal from 'deep-equal'; +import { KeyboardUtils } from 'react-native-keyboard-input'; import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; import Image from './Image'; @@ -20,9 +21,6 @@ import Emoji from './Emoji'; import messageStatus from '../../constants/messagesStatus'; import styles from './styles'; -const avatar = { marginRight: 10 }; -const flex = { flexDirection: 'row', flex: 1 }; - @connect(state => ({ message: state.messages.message, editing: state.messages.editing, @@ -43,7 +41,6 @@ export default class Message extends React.Component { editing: PropTypes.bool, actionsShow: PropTypes.func, errorActionsShow: PropTypes.func, - animate: PropTypes.bool, customEmojis: PropTypes.object, toggleReactionPicker: PropTypes.func, onReactionPress: PropTypes.func @@ -54,18 +51,6 @@ export default class Message extends React.Component { this.state = { reactionsModal: false }; this.onClose = this.onClose.bind(this); } - - componentWillMount() { - this._visibility = new Animated.Value(this.props.animate ? 0 : 1); - } - componentDidMount() { - if (this.props.animate) { - Animated.timing(this._visibility, { - toValue: 1, - duration: 300 - }).start(); - } - } componentWillReceiveProps() { this.extraStyle = this.extraStyle || {}; if (this.props.item.status === messageStatus.TEMP || this.props.item.status === messageStatus.ERROR) { @@ -84,7 +69,7 @@ export default class Message extends React.Component { } onPress = () => { - Keyboard.dismiss(); + KeyboardUtils.dismiss(); } onLongPress() { @@ -207,8 +192,8 @@ export default class Message extends React.Component { { reaction.usernames.length } @@ -239,18 +224,8 @@ export default class Message extends React.Component { const { item, message, editing, baseUrl, customEmojis } = this.props; - - const marginLeft = this._visibility.interpolate({ - inputRange: [0, 1], - outputRange: [-30, 0] - }); - const opacity = this._visibility.interpolate({ - inputRange: [0, 1], - outputRange: [0, 1] - }); const username = item.alias || item.u.username; const isEditing = message._id === item._id && editing; - const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; return ( @@ -263,11 +238,11 @@ export default class Message extends React.Component { style={[styles.message, isEditing ? styles.editing : null]} accessibilityLabel={accessibilityLabel} > - + {this.renderError()} - + : null } - + ); } diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 7752dea76..23911cc48 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -5,6 +5,10 @@ export default StyleSheet.create({ flexGrow: 1, flexShrink: 1 }, + flex: { + flexDirection: 'row', + flex: 1 + }, message: { padding: 12, paddingTop: 6, @@ -63,5 +67,8 @@ export default StyleSheet.create({ reactionCustomEmoji: { width: 15, height: 15 + }, + avatar: { + marginRight: 10 } }); diff --git a/app/presentation/KeyboardView.js b/app/presentation/KeyboardView.js index 6e405c664..82a5e1100 100644 --- a/app/presentation/KeyboardView.js +++ b/app/presentation/KeyboardView.js @@ -2,14 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ViewPropTypes } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { connect } from 'react-redux'; import scrollPersistTaps from '../utils/scrollPersistTaps'; -import { setKeyboardOpen, setKeyboardClosed } from '../actions/keyboard'; -@connect(null, dispatch => ({ - setKeyboardOpen: () => dispatch(setKeyboardOpen()), - setKeyboardClosed: () => dispatch(setKeyboardClosed()) -})) export default class KeyboardView extends React.PureComponent { static propTypes = { style: ViewPropTypes.style, @@ -19,9 +13,7 @@ export default class KeyboardView extends React.PureComponent { children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node - ]), - setKeyboardOpen: PropTypes.func, - setKeyboardClosed: PropTypes.func + ]) } render() { @@ -34,8 +26,6 @@ export default class KeyboardView extends React.PureComponent { alwaysBounceVertical={false} extraHeight={this.props.keyboardVerticalOffset} behavior='position' - onKeyboardWillShow={() => this.props.setKeyboardOpen()} - onKeyboardWillHide={() => this.props.setKeyboardClosed()} > {this.props.children} diff --git a/app/reducers/index.js b/app/reducers/index.js index 60c2be016..f7872a539 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -12,7 +12,6 @@ import app from './app'; import permissions from './permissions'; import customEmojis from './customEmojis'; import activeUsers from './activeUsers'; -import keyboard from './keyboard'; export default combineReducers({ settings, @@ -27,6 +26,5 @@ export default combineReducers({ rooms, permissions, customEmojis, - activeUsers, - keyboard + activeUsers }); diff --git a/app/reducers/keyboard.js b/app/reducers/keyboard.js deleted file mode 100644 index 0885be777..000000000 --- a/app/reducers/keyboard.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as types from '../actions/actionsTypes'; - -const initialState = { - isOpen: false -}; - -export default function messages(state = initialState, action) { - switch (action.type) { - case types.KEYBOARD.OPEN: - return { - ...state, - isOpen: true - }; - case types.KEYBOARD.CLOSE: - return { - ...state, - isOpen: false - }; - default: - return state; - } -} diff --git a/app/reducers/room.js b/app/reducers/room.js index 03b1fa898..4ed7c921b 100644 --- a/app/reducers/room.js +++ b/app/reducers/room.js @@ -1,7 +1,8 @@ import * as types from '../actions/actionsTypes'; const initialState = { - usersTyping: [] + usersTyping: [], + layoutAnimation: new Date() }; export default function room(state = initialState, action) { @@ -31,6 +32,11 @@ export default function room(state = initialState, action) { ...state, usersTyping: [...state.usersTyping.filter(user => user !== action.username)] }; + case types.ROOM.LAYOUT_ANIMATION: + return { + ...state, + layoutAnimation: new Date() + }; default: return state; } diff --git a/app/views/Photo.js b/app/views/Photo.js index e68597543..a13dd4adb 100644 --- a/app/views/Photo.js +++ b/app/views/Photo.js @@ -14,7 +14,7 @@ const styles = { } }; -export default class extends React.PureComponent { +export default class Photo extends React.PureComponent { static propTypes = { navigation: PropTypes.object.isRequired } diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index 5cf03f123..1402712f4 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -18,7 +18,7 @@ import { closeRoom } from '../../../actions/room'; }), dispatch => ({ close: () => dispatch(closeRoom()) })) -export default class extends React.PureComponent { +export default class RoomHeaderView extends React.PureComponent { static propTypes = { close: PropTypes.func.isRequired, navigation: PropTypes.object.isRequired, diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js index 9c89b46f5..500e693bb 100644 --- a/app/views/RoomView/ListView.js +++ b/app/views/RoomView/ListView.js @@ -1,7 +1,7 @@ import { ListView as OldList } from 'realm/react-native'; import React from 'react'; import cloneReferencedElement from 'react-clone-referenced-element'; -import { ScrollView, ListView as OldList2 } from 'react-native'; +import { ScrollView, ListView as OldList2, LayoutAnimation } from 'react-native'; import moment from 'moment'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -12,6 +12,7 @@ import styles from './styles'; import debounce from '../../utils/debounce'; import Typing from '../../containers/Typing'; import database from '../../lib/realm'; +import scrollPersistTaps from '../../utils/scrollPersistTaps'; const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100; @@ -28,10 +29,6 @@ export class DataSource extends OldList.DataSource { const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); -@connect(state => ({ - lastOpen: state.room.lastOpen -})) - export class List extends React.Component { static propTypes = { onEndReached: PropTypes.func, @@ -54,6 +51,9 @@ export class List extends React.Component { shouldComponentUpdate(nextProps) { return this.props.end !== nextProps.end; } + componentWillUpdate() { + LayoutAnimation.easeInEaseOut(); + } updateState = debounce(() => { // this.setState({ this.dataSource = this.dataSource.cloneWithRows(this.data); @@ -72,12 +72,14 @@ export class List extends React.Component { dataSource={this.dataSource} renderRow={item => this.props.renderRow(item)} initialListSize={10} - keyboardShouldPersistTaps='always' - keyboardDismissMode='none' + {...scrollPersistTaps} />); } } +@connect(state => ({ + lastOpen: state.room.lastOpen +})) export class ListView extends OldList2 { constructor(props) { super(props); diff --git a/app/views/RoomView/ReactionPicker.js b/app/views/RoomView/ReactionPicker.js index 96ff5fd2d..7c5592cb1 100644 --- a/app/views/RoomView/ReactionPicker.js +++ b/app/views/RoomView/ReactionPicker.js @@ -17,7 +17,7 @@ const tabEmojiStyle = { fontSize: 15 }; toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) })) @responsive -export default class extends React.Component { +export default class ReactionPicker extends React.Component { static propTypes = { window: PropTypes.any, showReactionPicker: PropTypes.bool, diff --git a/app/views/RoomView/UnreadSeparator.js b/app/views/RoomView/UnreadSeparator.js index c2856e677..f5384fc29 100644 --- a/app/views/RoomView/UnreadSeparator.js +++ b/app/views/RoomView/UnreadSeparator.js @@ -1,5 +1,5 @@ import React from 'react'; -import { View, StyleSheet, Text } from 'react-native'; +import { View, StyleSheet, Text, LayoutAnimation } from 'react-native'; const styles = StyleSheet.create({ firstUnread: { @@ -22,11 +22,16 @@ const styles = StyleSheet.create({ } }); -const UnreadSeparator = () => ( - - - unread messages - -); - -export default UnreadSeparator; +export default class UnreadSeparator extends React.PureComponent { + componentWillUnmount() { + LayoutAnimation.linear(); + } + render() { + return ( + + + unread messages + + ); + } +} diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index dd76d5d43..4d32ca07d 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -1,23 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Text, View, Button, SafeAreaView, Platform, Keyboard } from 'react-native'; +import { Text, View, Button, LayoutAnimation } from 'react-native'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import equal from 'deep-equal'; -import KeyboardSpacer from 'react-native-keyboard-spacer'; import { List } from './ListView'; import * as actions from '../../actions'; import { openRoom, setLastOpen } from '../../actions/room'; import { editCancel, toggleReactionPicker } from '../../actions/messages'; -import { setKeyboardOpen, setKeyboardClosed } from '../../actions/keyboard'; import database from '../../lib/realm'; import RocketChat from '../../lib/rocketchat'; import Message from '../../containers/message'; import MessageActions from '../../containers/MessageActions'; import MessageErrorActions from '../../containers/MessageErrorActions'; import MessageBox from '../../containers/MessageBox'; - import Header from '../../containers/Header'; import RoomsHeader from './Header'; import ReactionPicker from './ReactionPicker'; @@ -30,16 +27,15 @@ import styles from './styles'; Message_TimeFormat: state.settings.Message_TimeFormat, loading: state.messages.isFetching, user: state.login.user, - actionMessage: state.messages.actionMessage + actionMessage: state.messages.actionMessage, + layoutAnimation: state.room.layoutAnimation }), dispatch => ({ actions: bindActionCreators(actions, dispatch), openRoom: room => dispatch(openRoom(room)), editCancel: () => dispatch(editCancel()), setLastOpen: date => dispatch(setLastOpen(date)), - toggleReactionPicker: message => dispatch(toggleReactionPicker(message)), - setKeyboardOpen: () => dispatch(setKeyboardOpen()), - setKeyboardClosed: () => dispatch(setKeyboardClosed()) + toggleReactionPicker: message => dispatch(toggleReactionPicker(message)) }) ) export default class RoomView extends React.Component { @@ -56,8 +52,7 @@ export default class RoomView extends React.Component { loading: PropTypes.bool, actionMessage: PropTypes.object, toggleReactionPicker: PropTypes.func.isRequired, - setKeyboardOpen: PropTypes.func, - setKeyboardClosed: PropTypes.func + layoutAnimation: PropTypes.instanceOf(Date) }; static navigationOptions = ({ navigation }) => ({ @@ -72,7 +67,6 @@ export default class RoomView extends React.Component { this.name = props.name || props.navigation.state.params.name || props.navigation.state.params.room.name; - this.opened = new Date(); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.state = { loaded: true, @@ -82,12 +76,12 @@ export default class RoomView extends React.Component { this.onReactionPress = this.onReactionPress.bind(this); } - componentWillMount() { + async componentWillMount() { this.props.navigation.setParams({ title: this.name }); this.updateRoom(); - this.props.openRoom({ rid: this.rid, name: this.name, ls: this.state.room.ls }); + await this.props.openRoom({ rid: this.rid, name: this.name, ls: this.state.room.ls }); if (this.state.room.alert || this.state.room.unread || this.state.room.userMentions) { this.props.setLastOpen(this.state.room.ls); } else { @@ -95,8 +89,11 @@ export default class RoomView extends React.Component { } this.rooms.addListener(this.updateRoom); - this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => this.props.setKeyboardOpen()); - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => this.props.setKeyboardClosed()); + } + componentWillReceiveProps(nextProps) { + if (this.props.layoutAnimation !== nextProps.layoutAnimation) { + LayoutAnimation.spring(); + } } shouldComponentUpdate(nextProps, nextState) { return !(equal(this.props, nextProps) && equal(this.state, nextState)); @@ -104,8 +101,6 @@ export default class RoomView extends React.Component { componentWillUnmount() { clearTimeout(this.timer); this.rooms.removeAllListeners(); - this.keyboardDidShowListener.remove(); - this.keyboardDidHideListener.remove(); this.props.editCancel(); } @@ -141,9 +136,11 @@ export default class RoomView extends React.Component { this.setState({ room: this.rooms[0] }); } - sendMessage = message => RocketChat.sendMessage(this.rid, message).then(() => { - this.props.setLastOpen(null); - }); + sendMessage = (message) => { + RocketChat.sendMessage(this.rid, message).then(() => { + this.props.setLastOpen(null); + }); + }; joinRoom = async() => { await RocketChat.joinRoom(this.props.rid); @@ -157,7 +154,6 @@ export default class RoomView extends React.Component { key={item._id} item={item} reactions={JSON.parse(JSON.stringify(item.reactions))} - animate={this.opened.toISOString() < item.ts.toISOString()} baseUrl={this.props.Site_Url} Message_TimeFormat={this.props.Message_TimeFormat} user={this.props.user} @@ -196,20 +192,18 @@ export default class RoomView extends React.Component { return ( - - this.renderItem(item)} - /> - + this.renderItem(item)} + /> {this.renderFooter()} {this.state.room._id ? : null} - {Platform.OS === 'ios' ? : null} ); } diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js index 6b68d2333..089c56fc8 100644 --- a/app/views/RoomView/styles.js +++ b/app/views/RoomView/styles.js @@ -1,4 +1,4 @@ -import { StyleSheet, Platform } from 'react-native'; +import { StyleSheet } from 'react-native'; export default StyleSheet.create({ typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 }, @@ -37,7 +37,7 @@ export default StyleSheet.create({ reactionPickerContainer: { // width: width - 20, // height: width - 20, - paddingHorizontal: Platform.OS === 'android' ? 11 : 10, + // paddingHorizontal: Platform.OS === 'android' ? 11 : 10, backgroundColor: '#F7F7F7', borderRadius: 4, flexDirection: 'column' diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index 52b4d5064..af91e2e85 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -20,7 +20,7 @@ import styles from './styles'; }), dispatch => ({ setSearch: searchText => dispatch(setSearch(searchText)) })) -export default class extends React.Component { +export default class RoomsListHeaderView extends React.Component { static propTypes = { navigation: PropTypes.object.isRequired, user: PropTypes.object.isRequired, diff --git a/app/views/SelectUsersView.js b/app/views/SelectUsersView.js index 667ecccf0..17b908899 100644 --- a/app/views/SelectUsersView.js +++ b/app/views/SelectUsersView.js @@ -69,7 +69,7 @@ const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }); resetCreateChannel: () => dispatch(createChannelActions.reset()) }) ) -export default class RoomsListView extends React.Component { +export default class SelectUsersView extends React.Component { static propTypes = { navigation: PropTypes.object.isRequired, Site_Url: PropTypes.string, diff --git a/index.android.js b/index.android.js index c7a8b8bc9..4fade3c08 100644 --- a/index.android.js +++ b/index.android.js @@ -5,6 +5,8 @@ import { AppRegistry } from 'react-native'; import './app/push'; import RocketChat from './app/index'; +// UIManager.setLayoutAnimationEnabledExperimental(true); + // import './app/ReactotronConfig'; // import { AppRegistry } from 'react-native'; // import Routes from './app/routes'; diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 167166fe6..23a4c8279 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -47,11 +47,13 @@ 647660C6B6A340C7BD4D1099 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = A18EFC3B0CFE40E0918A8F0C /* EvilIcons.ttf */; }; 70A8D9B456894EFFAF027CAB /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7A30DA4B2D474348824CD05B /* FontAwesome.ttf */; }; 77C35F50C01C43668188886C /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A0EEFAF8AB14F5B9E796CDD /* libRNVectorIcons.a */; }; + 7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 8A159EDB97C44E52AF62D69C /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA50CE47374C4C35BE6D9D58 /* libRNSVG.a */; }; 8ECBD927DDAC4987B98E102E /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */; }; AE5D35882AE04CC29630FB3D /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = DC6EE17B5550465E98C70FF0 /* Entypo.ttf */; }; B88F586F1FBF57F600B352B8 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B88F58461FBF55E200B352B8 /* libRCTPushNotification.a */; }; + B8971BB2202A093B0000D245 /* libKeyboardTrackingView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B8971BB1202A091D0000D245 /* libKeyboardTrackingView.a */; }; B8C682A81FD850F4003A12C8 /* icomoon.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8C682611FD84CEF003A12C8 /* icomoon.ttf */; }; B8C682AC1FD8511D003A12C8 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1B0746E708284151B8AD1198 /* Ionicons.ttf */; }; B8C682AD1FD8511E003A12C8 /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1B0746E708284151B8AD1198 /* Ionicons.ttf */; }; @@ -296,6 +298,13 @@ remoteGlobalIDString = 134814201AA4EA6300B7C361; remoteInfo = RCTLinking; }; + 7A430E1D20238C02008F55BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 39DF4FE71E00394E00F5B4B2; + remoteInfo = RCTCustomInputController; + }; 7A7F5C981FCC982500024129 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD0379F2BCE84C968538CDAF /* RCTVideo.xcodeproj */; @@ -366,6 +375,13 @@ remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04; remoteInfo = "privatedata-tvOS"; }; + B8971BB0202A091D0000D245 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D834CED81CC64F2400FA5668; + remoteInfo = KeyboardTrackingView; + }; B8E79A8D1F3CCC6D005B464F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4CD38E4891ED4601B7481448 /* RNFetchBlob.xcodeproj */; @@ -436,6 +452,7 @@ 6533FB90166345D29F1B91C0 /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 7A30DA4B2D474348824CD05B /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; }; + 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCustomInputController.xcodeproj; path = "../node_modules/react-native-keyboard-input/lib/ios/RCTCustomInputController.xcodeproj"; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 8A2DD67ADD954AD9873F45FC /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; }; 9A1E1766CCB84C91A62BD5A6 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; }; @@ -444,6 +461,7 @@ B2607FA180F14E6584301101 /* libSplashScreen.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libSplashScreen.a; sourceTree = ""; }; B37C79D9BD0742CE936B6982 /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; B88F58361FBF55E200B352B8 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; + B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = KeyboardTrackingView.xcodeproj; path = "../node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingView.xcodeproj"; sourceTree = ""; }; B8C682611FD84CEF003A12C8 /* icomoon.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = icomoon.ttf; path = ../resources/fonts/icomoon.ttf; sourceTree = ""; }; BAAE4B947F5D44959F0A9D5A /* libRNZeroconf.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNZeroconf.a; sourceTree = ""; }; C23AEF1D9EBE4A38A1A6B97B /* RNSVG.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = ""; }; @@ -467,6 +485,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B8971BB2202A093B0000D245 /* libKeyboardTrackingView.a in Frameworks */, + 7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */, 146834051AC3E58100842450 /* libReact.a in Frameworks */, B88F586F1FBF57F600B352B8 /* libRCTPushNotification.a in Frameworks */, 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */, @@ -674,6 +694,14 @@ name = Products; sourceTree = ""; }; + 7A430E1720238C01008F55BC /* Products */ = { + isa = PBXGroup; + children = ( + 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */, + ); + name = Products; + sourceTree = ""; + }; 7A7F5C831FCC982500024129 /* Products */ = { isa = PBXGroup; children = ( @@ -694,6 +722,8 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */, + 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */, B88F58361FBF55E200B352B8 /* RCTPushNotification.xcodeproj */, 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, @@ -780,6 +810,14 @@ name = Products; sourceTree = ""; }; + B8971BAD202A091D0000D245 /* Products */ = { + isa = PBXGroup; + children = ( + B8971BB1202A091D0000D245 /* libKeyboardTrackingView.a */, + ); + name = Products; + sourceTree = ""; + }; B8E79A681F3CCC69005B464F /* Recovered References */ = { isa = PBXGroup; children = ( @@ -952,6 +990,10 @@ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectReferences = ( + { + ProductGroup = B8971BAD202A091D0000D245 /* Products */; + ProjectRef = B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */; + }, { ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; @@ -960,6 +1002,10 @@ ProductGroup = 5E91572E1DD0AC6500FF2AA8 /* Products */; ProjectRef = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */; }, + { + ProductGroup = 7A430E1720238C01008F55BC /* Products */; + ProjectRef = 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */; + }, { ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; @@ -1261,6 +1307,13 @@ remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTCustomInputController.a; + remoteRef = 7A430E1D20238C02008F55BC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7A7F5C991FCC982500024129 /* libRCTVideo.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1331,6 +1384,13 @@ remoteRef = B88F58661FBF55E200B352B8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + B8971BB1202A091D0000D245 /* libKeyboardTrackingView.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libKeyboardTrackingView.a; + remoteRef = B8971BB0202A091D0000D245 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; B8E79A8E1F3CCC6D005B464F /* libRNFetchBlob.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/package-lock.json b/package-lock.json index fb9a3f7ab..57ab9160a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12568,10 +12568,20 @@ "react-native-iphone-x-helper": "1.0.1" } }, - "react-native-keyboard-spacer": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/react-native-keyboard-spacer/-/react-native-keyboard-spacer-0.4.1.tgz", - "integrity": "sha1-RvGKMgQyCYol6p+on1FD3SVNMy0=" + "react-native-keyboard-input": { + "version": "git+https://github.com/RocketChat/react-native-keyboard-input.git#38273b0513f69a5e6e0719f65a675f9f2b5ee883", + "requires": { + "lodash": "4.17.4", + "react-native-keyboard-tracking-view": "git+https://github.com/RocketChat/react-native-keyboard-tracking-view.git#3a4084f0a1063e23ae6435facdf1f79152558d15" + }, + "dependencies": { + "react-native-keyboard-tracking-view": { + "version": "git+https://github.com/RocketChat/react-native-keyboard-tracking-view.git#3a4084f0a1063e23ae6435facdf1f79152558d15" + } + } + }, + "react-native-keyboard-tracking-view": { + "version": "git+https://github.com/RocketChat/react-native-keyboard-tracking-view.git#3a4084f0a1063e23ae6435facdf1f79152558d15" }, "react-native-loading-spinner-overlay": { "version": "0.5.2", @@ -12624,9 +12634,9 @@ } }, "react-native-optimized-flatlist": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/react-native-optimized-flatlist/-/react-native-optimized-flatlist-1.0.3.tgz", - "integrity": "sha1-tFN58lpXu05vhZwZDZmEexgR4Ak=", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-native-optimized-flatlist/-/react-native-optimized-flatlist-1.0.4.tgz", + "integrity": "sha512-PMoZRJAHKzd/ahYKUzt43AJ+kVhHpOSTvBhJdQqooZXw312xADWpR7iDvBAbBiRGkmk0yM4GJacd9TMft6q/Gg==", "requires": { "prop-types": "15.6.0" } diff --git a/package.json b/package.json index e6bc7f8dc..8cf7d9a1a 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,12 @@ "react-native-image-picker": "^0.26.7", "react-native-img-cache": "^1.5.2", "react-native-keyboard-aware-scroll-view": "^0.4.1", - "react-native-keyboard-spacer": "^0.4.1", + "react-native-keyboard-input": "git+https://github.com/RocketChat/react-native-keyboard-input.git", + "react-native-keyboard-tracking-view": "git+https://github.com/RocketChat/react-native-keyboard-tracking-view.git", "react-native-loading-spinner-overlay": "^0.5.2", "react-native-meteor": "^1.2.0", "react-native-modal": "^4.1.1", - "react-native-optimized-flatlist": "^1.0.3", + "react-native-optimized-flatlist": "^1.0.4", "react-native-push-notification": "^3.0.1", "react-native-responsive-ui": "^1.1.1", "react-native-scrollable-tab-view": "^0.8.0",