diff --git a/app/containers/MessageBox/AnimatedContainer.js b/app/containers/MessageBox/AnimatedContainer.js
new file mode 100644
index 000000000..eb5cbbe47
--- /dev/null
+++ b/app/containers/MessageBox/AnimatedContainer.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Animated } from 'react-native';
+
+export default class MessageBox extends React.PureComponent {
+ static propTypes = {
+ subview: PropTypes.object.isRequired,
+ visible: PropTypes.bool.isRequired,
+ messageboxHeight: PropTypes.number.isRequired
+ }
+
+ constructor(props) {
+ super(props);
+ this.animatedBottom = new Animated.Value(0);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.visible === nextProps.visible) {
+ return;
+ }
+ if (nextProps.visible) {
+ return this.show();
+ }
+ this.hide();
+ }
+
+ show() {
+ this.animatedBottom.setValue(0);
+ Animated.timing(this.animatedBottom, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true
+ }).start();
+ }
+
+ hide() {
+ Animated.timing(this.animatedBottom, {
+ toValue: 0,
+ duration: 300,
+ useNativeDriver: true
+ }).start();
+ }
+
+ render() {
+ const bottom = this.animatedBottom.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, -this.props.messageboxHeight - 200]
+ });
+
+ return (
+
+ {this.props.subview}
+
+ );
+ }
+}
diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js
index e84c9aef8..edae5f168 100644
--- a/app/containers/MessageBox/index.js
+++ b/app/containers/MessageBox/index.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { View, TextInput, SafeAreaView, Platform } from 'react-native';
+import { View, TextInput, SafeAreaView, Platform, FlatList, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import ImagePicker from 'react-native-image-picker';
import { connect } from 'react-redux';
@@ -9,10 +9,21 @@ import RocketChat from '../../lib/rocketchat';
import { editRequest, editCancel, clearInput } from '../../actions/messages';
import styles from './style';
import MyIcon from '../icons';
+import realm from '../../lib/realm';
+import Avatar from '../Avatar';
+import AnimatedContainer from './AnimatedContainer';
+
+const MENTIONS_TRACKING_TYPE_USERS = '@';
+
+const onlyUnique = function onlyUnique(value, index, self) {
+ return self.indexOf(({ _id }) => value._id === _id) === index;
+};
+
@connect(state => ({
room: state.room,
message: state.messages.message,
- editing: state.messages.editing
+ editing: state.messages.editing,
+ baseUrl: state.settings.Site_Url
}), dispatch => ({
editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)),
@@ -25,6 +36,7 @@ export default class MessageBox extends React.Component {
rid: PropTypes.string.isRequired,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired,
+ baseUrl: PropTypes.string.isRequired,
message: PropTypes.object,
editing: PropTypes.bool,
typing: PropTypes.func,
@@ -35,22 +47,49 @@ export default class MessageBox extends React.Component {
super(props);
this.state = {
height: 20,
- text: ''
+ messageboxHeight: 0,
+ text: '',
+ mentions: [],
+ showAnimatedContainer: false
};
+ this.users = [];
+ this.rooms = [];
}
-
componentWillReceiveProps(nextProps) {
- if (this.props.message !== nextProps.message && nextProps.message) {
- this.component.setNativeProps({ text: nextProps.message.msg });
+ if (this.props.message !== nextProps.message && nextProps.message.msg) {
+ this.setState({ text: nextProps.message.msg });
this.component.focus();
} else if (!nextProps.message) {
- this.component.setNativeProps({ text: '' });
+ this.setState({ text: '' });
}
}
+
+ onChange() {
+ requestAnimationFrame(() => {
+ const { start, end } = this.component._lastNativeSelection;
+
+ const cursor = Math.max(start, end);
+
+ const text = this.component._lastNativeText;
+
+ const regexp = /(#|@)([a-z._-]+)$/im;
+
+ const result = text.substr(0, cursor).match(regexp);
+
+ if (!result) {
+ return this.stopTrackingMention();
+ }
+ const [, lastChar, name] = result;
+
+ this.identifyMentionKeyword(name, lastChar);
+ });
+ }
+
+
onChangeText(text) {
this.setState({ text });
- this.props.typing(text.length > 0);
}
+
get leftButtons() {
const { editing } = this.props;
if (editing) {
@@ -79,14 +118,14 @@ export default class MessageBox extends React.Component {
get rightButtons() {
const icons = [];
- if (this.state.text.length) {
+ if (this.state.text) {
icons.push( this.submit(this.component._lastNativeText)}
+ onPress={() => this.submit(this.state.text)}
/>);
}
icons.push( {
this.setState({ height: height + (Platform.OS === 'ios' ? 0 : 0) });
}
+
addFile = () => {
const options = {
customButtons: [{
@@ -133,14 +170,14 @@ export default class MessageBox extends React.Component {
}
editCancel() {
this.props.editCancel();
- this.component.setNativeProps({ text: '' });
+ this.setState({ text: '' });
}
openEmoji() {
this.setState({ emoji: !this.state.emoji });
}
submit(message) {
- this.component.setNativeProps({ text: '' });
this.setState({ text: '' });
+ this.stopTrackingMention();
requestAnimationFrame(() => {
this.props.typing(false);
if (message.trim() === '') {
@@ -159,28 +196,185 @@ export default class MessageBox extends React.Component {
});
}
+ async _getUsers(keyword) {
+ this.users = realm.objects('users');
+ if (keyword) {
+ this.users = this.users.filtered('username CONTAINS[c] $0', keyword);
+ }
+ this.setState({ mentions: this.users.slice() });
+
+ const usernames = [];
+
+ if (keyword && this.users.length > 7) {
+ return;
+ }
+
+ this.users.forEach(user => usernames.push(user.username));
+
+ if (this.oldPromise) {
+ this.oldPromise();
+ }
+ try {
+ const results = await Promise.race([
+ RocketChat.spotlight(keyword, usernames, { users: true }),
+ new Promise((resolve, reject) => (this.oldPromise = reject))
+ ]);
+ realm.write(() => {
+ results.users.forEach((user) => {
+ user._server = {
+ id: this.props.baseUrl,
+ current: true
+ };
+ realm.create('users', user, true);
+ });
+ });
+ } catch (e) {
+ console.log('spotlight canceled');
+ } finally {
+ delete this.oldPromise;
+ this.users = realm.objects('users').filtered('username CONTAINS[c] $0', keyword);
+ this.setState({ mentions: this.users.slice() });
+ }
+ }
+
+ async _getRooms(keyword = '') {
+ this.roomsCache = this.roomsCache || [];
+ this.rooms = realm.objects('subscriptions')
+ .filtered('_server.id = $0 AND t != $1', this.props.baseUrl, 'd');
+ if (keyword) {
+ this.rooms = this.rooms.filtered('name CONTAINS[c] $0', keyword);
+ }
+
+ const rooms = [];
+ this.rooms.forEach(room => rooms.push(room));
+
+ this.roomsCache.forEach((room) => {
+ if (room.name && room.name.toUpperCase().indexOf(keyword.toUpperCase()) !== -1) {
+ rooms.push(room);
+ }
+ });
+
+ if (rooms.length > 3) {
+ this.setState({ mentions: rooms });
+ return;
+ }
+
+ if (this.oldPromise) {
+ this.oldPromise();
+ }
+
+ try {
+ const results = await Promise.race([
+ RocketChat.spotlight(keyword, [...rooms, ...this.roomsCache].map(r => r.name), { rooms: true }),
+ new Promise((resolve, reject) => (this.oldPromise = reject))
+ ]);
+ this.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique);
+ this.setState({ mentions: [...rooms.slice(), ...results.rooms] });
+ } catch (e) {
+ console.log('spotlight canceled');
+ } finally {
+ delete this.oldPromise;
+ }
+ }
+
+ stopTrackingMention() {
+ this.setState({
+ showAnimatedContainer: false,
+ mentions: []
+ });
+ this.users = [];
+ this.rooms = [];
+ }
+
+ identifyMentionKeyword(keyword, type) {
+ this.updateMentions(keyword, type);
+ this.setState({
+ showAnimatedContainer: true
+ });
+ }
+
+ updateMentions = (keyword, type) => {
+ if (type === MENTIONS_TRACKING_TYPE_USERS) {
+ this._getUsers(keyword);
+ } else {
+ this._getRooms(keyword);
+ }
+ }
+
+ _onPressMention(item) {
+ const msg = this.component._lastNativeText;
+
+ const { start, end } = this.component._lastNativeSelection;
+
+ const cursor = Math.max(start, end);
+
+ const regexp = /([a-z._-]+)$/im;
+
+ const result = msg.substr(0, cursor).replace(regexp, '');
+ const text = `${ result }${ item.username || item.name } ${ msg.slice(cursor) }`;
+ this.component.setNativeProps({ text });
+ this.setState({ text });
+ this.component.focus();
+ requestAnimationFrame(() => this.stopTrackingMention());
+ }
+ renderMentionItem = item => (
+ this._onPressMention(item)}
+ >
+
+ {item.username || item.name }
+
+ )
+ renderMentions() {
+ const usersList = (
+ this.renderMentionItem(item)}
+ keyExtractor={item => item._id}
+ keyboardShouldPersistTaps='always'
+ keyboardDismissMode='interactive'
+ />
+ );
+ const { showAnimatedContainer, messageboxHeight } = this.state;
+ return ;
+ }
render() {
const { height } = this.state;
return (
-
-
- {this.leftButtons}
- this.component = component}
- style={[styles.textBoxInput, { height }]}
- returnKeyType='default'
- blurOnSubmit={false}
- placeholder='New Message'
- onChangeText={text => this.onChangeText(text)}
- underlineColorAndroid='transparent'
- defaultValue=''
- multiline
- placeholderTextColor='#9EA2A8'
- onContentSizeChange={e => this.updateSize(e.nativeEvent.contentSize.height)}
- />
- {this.rightButtons}
-
-
+
+ this.setState({ messageboxHeight: event.nativeEvent.layout.height })}
+ >
+
+ {this.leftButtons}
+ this.component = component}
+ style={[styles.textBoxInput, { height }]}
+ returnKeyType='default'
+ blurOnSubmit={false}
+ placeholder='New Message'
+ onChangeText={text => this.onChangeText(text)}
+ onChange={event => this.onChange(event)}
+ value={this.state.text}
+ underlineColorAndroid='transparent'
+ defaultValue=''
+ multiline
+ placeholderTextColor='#9EA2A8'
+ onContentSizeChange={e => this.updateSize(e.nativeEvent.contentSize.height)}
+ />
+ {this.rightButtons}
+
+
+ {this.renderMentions()}
+
);
}
}
diff --git a/app/containers/MessageBox/style.js b/app/containers/MessageBox/style.js
index d6c521df7..03c076729 100644
--- a/app/containers/MessageBox/style.js
+++ b/app/containers/MessageBox/style.js
@@ -1,5 +1,6 @@
import { StyleSheet } from 'react-native';
+const MENTION_HEIGHT = 50;
export default StyleSheet.create({
textBox: {
@@ -9,7 +10,8 @@ export default StyleSheet.create({
borderTopWidth: 1,
borderTopColor: '#D8D8D8',
paddingHorizontal: 15,
- paddingVertical: 15
+ paddingVertical: 15,
+ zIndex: 2
},
safeAreaView: {
flexDirection: 'row',
@@ -62,5 +64,20 @@ export default StyleSheet.create({
borderBottomWidth: 1,
borderBottomColor: '#ECECEC',
color: '#2F343D'
+ },
+ mentionList: {
+ maxHeight: MENTION_HEIGHT * 4,
+ borderTopColor: '#ECECEC',
+ borderTopWidth: 1,
+ paddingHorizontal: 5,
+ backgroundColor: '#fff'
+ },
+ mentionItem: {
+ height: MENTION_HEIGHT,
+ backgroundColor: '#F7F8FA',
+ borderBottomWidth: 1,
+ borderBottomColor: '#ECECEC',
+ flexDirection: 'row',
+ alignItems: 'center'
}
});
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 884d1b3fb..0759037fc 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -346,8 +346,8 @@ const RocketChat = {
return RocketChat._sendMessageCall(message);
},
- spotlight(search, usernames) {
- return call('spotlight', search, usernames);
+ spotlight(search, usernames, type) {
+ return call('spotlight', search, usernames, type);
},
createDirectMessage(username) {
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index e8a2647aa..ae1a14657 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -184,6 +184,8 @@ export default class RoomView extends React.Component {
dataSource={this.state.dataSource}
renderRow={item => this.renderItem({ item })}
initialListSize={10}
+ keyboardShouldPersistTaps='always'
+ keyboardDismissMode='interactive'
/>
{this.renderFooter()}