From 9ea5c1b765cc7785e164fb0da1501f68cea0e767 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Tue, 30 Jan 2018 17:48:26 -0200 Subject: [PATCH] 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": {