parent
11d9a66752
commit
157b85c2cb
|
@ -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 (
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: -200,
|
||||||
|
zIndex: 1,
|
||||||
|
transform: [{ translateY: bottom }]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{this.props.subview}
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 Icon from 'react-native-vector-icons/Ionicons';
|
||||||
import ImagePicker from 'react-native-image-picker';
|
import ImagePicker from 'react-native-image-picker';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -9,10 +9,21 @@ import RocketChat from '../../lib/rocketchat';
|
||||||
import { editRequest, editCancel, clearInput } from '../../actions/messages';
|
import { editRequest, editCancel, clearInput } from '../../actions/messages';
|
||||||
import styles from './style';
|
import styles from './style';
|
||||||
import MyIcon from '../icons';
|
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 => ({
|
@connect(state => ({
|
||||||
room: state.room,
|
room: state.room,
|
||||||
message: state.messages.message,
|
message: state.messages.message,
|
||||||
editing: state.messages.editing
|
editing: state.messages.editing,
|
||||||
|
baseUrl: state.settings.Site_Url
|
||||||
}), dispatch => ({
|
}), dispatch => ({
|
||||||
editCancel: () => dispatch(editCancel()),
|
editCancel: () => dispatch(editCancel()),
|
||||||
editRequest: message => dispatch(editRequest(message)),
|
editRequest: message => dispatch(editRequest(message)),
|
||||||
|
@ -25,6 +36,7 @@ export default class MessageBox extends React.Component {
|
||||||
rid: PropTypes.string.isRequired,
|
rid: PropTypes.string.isRequired,
|
||||||
editCancel: PropTypes.func.isRequired,
|
editCancel: PropTypes.func.isRequired,
|
||||||
editRequest: PropTypes.func.isRequired,
|
editRequest: PropTypes.func.isRequired,
|
||||||
|
baseUrl: PropTypes.string.isRequired,
|
||||||
message: PropTypes.object,
|
message: PropTypes.object,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
typing: PropTypes.func,
|
typing: PropTypes.func,
|
||||||
|
@ -35,22 +47,49 @@ export default class MessageBox extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
height: 20,
|
height: 20,
|
||||||
text: ''
|
messageboxHeight: 0,
|
||||||
|
text: '',
|
||||||
|
mentions: [],
|
||||||
|
showAnimatedContainer: false
|
||||||
};
|
};
|
||||||
|
this.users = [];
|
||||||
|
this.rooms = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.message !== nextProps.message && nextProps.message) {
|
if (this.props.message !== nextProps.message && nextProps.message.msg) {
|
||||||
this.component.setNativeProps({ text: nextProps.message.msg });
|
this.setState({ text: nextProps.message.msg });
|
||||||
this.component.focus();
|
this.component.focus();
|
||||||
} else if (!nextProps.message) {
|
} 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) {
|
onChangeText(text) {
|
||||||
this.setState({ text });
|
this.setState({ text });
|
||||||
this.props.typing(text.length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get leftButtons() {
|
get leftButtons() {
|
||||||
const { editing } = this.props;
|
const { editing } = this.props;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
|
@ -79,14 +118,14 @@ export default class MessageBox extends React.Component {
|
||||||
get rightButtons() {
|
get rightButtons() {
|
||||||
const icons = [];
|
const icons = [];
|
||||||
|
|
||||||
if (this.state.text.length) {
|
if (this.state.text) {
|
||||||
icons.push(<MyIcon
|
icons.push(<MyIcon
|
||||||
style={[styles.actionButtons, { color: '#1D74F5' }]}
|
style={[styles.actionButtons, { color: '#1D74F5' }]}
|
||||||
name='send'
|
name='send'
|
||||||
key='sendIcon'
|
key='sendIcon'
|
||||||
accessibilityLabel='Send message'
|
accessibilityLabel='Send message'
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
onPress={() => this.submit(this.component._lastNativeText)}
|
onPress={() => this.submit(this.state.text)}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
icons.push(<MyIcon
|
icons.push(<MyIcon
|
||||||
|
@ -100,12 +139,10 @@ export default class MessageBox extends React.Component {
|
||||||
return icons;
|
return icons;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get placeholder() {
|
|
||||||
// return `New Message`.substring(0, 35);
|
|
||||||
// }
|
|
||||||
updateSize = (height) => {
|
updateSize = (height) => {
|
||||||
this.setState({ height: height + (Platform.OS === 'ios' ? 0 : 0) });
|
this.setState({ height: height + (Platform.OS === 'ios' ? 0 : 0) });
|
||||||
}
|
}
|
||||||
|
|
||||||
addFile = () => {
|
addFile = () => {
|
||||||
const options = {
|
const options = {
|
||||||
customButtons: [{
|
customButtons: [{
|
||||||
|
@ -133,14 +170,14 @@ export default class MessageBox extends React.Component {
|
||||||
}
|
}
|
||||||
editCancel() {
|
editCancel() {
|
||||||
this.props.editCancel();
|
this.props.editCancel();
|
||||||
this.component.setNativeProps({ text: '' });
|
this.setState({ text: '' });
|
||||||
}
|
}
|
||||||
openEmoji() {
|
openEmoji() {
|
||||||
this.setState({ emoji: !this.state.emoji });
|
this.setState({ emoji: !this.state.emoji });
|
||||||
}
|
}
|
||||||
submit(message) {
|
submit(message) {
|
||||||
this.component.setNativeProps({ text: '' });
|
|
||||||
this.setState({ text: '' });
|
this.setState({ text: '' });
|
||||||
|
this.stopTrackingMention();
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.props.typing(false);
|
this.props.typing(false);
|
||||||
if (message.trim() === '') {
|
if (message.trim() === '') {
|
||||||
|
@ -159,10 +196,163 @@ 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 => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mentionItem}
|
||||||
|
onPress={() => this._onPressMention(item)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
style={{ margin: 8 }}
|
||||||
|
text={item.username || item.name}
|
||||||
|
size={30}
|
||||||
|
baseUrl={this.props.baseUrl}
|
||||||
|
/>
|
||||||
|
<Text>{item.username || item.name }</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
renderMentions() {
|
||||||
|
const usersList = (
|
||||||
|
<FlatList
|
||||||
|
style={styles.mentionList}
|
||||||
|
data={this.state.mentions}
|
||||||
|
renderItem={({ item }) => this.renderMentionItem(item)}
|
||||||
|
keyExtractor={item => item._id}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
keyboardDismissMode='interactive'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const { showAnimatedContainer, messageboxHeight } = this.state;
|
||||||
|
return <AnimatedContainer visible={showAnimatedContainer} subview={usersList} messageboxHeight={messageboxHeight} />;
|
||||||
|
}
|
||||||
render() {
|
render() {
|
||||||
const { height } = this.state;
|
const { height } = this.state;
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.textBox, (this.props.editing ? styles.editing : null)]}>
|
<View>
|
||||||
|
<SafeAreaView
|
||||||
|
style={[styles.textBox, (this.props.editing ? styles.editing : null)]}
|
||||||
|
onLayout={event => this.setState({ messageboxHeight: event.nativeEvent.layout.height })}
|
||||||
|
>
|
||||||
<View style={styles.textArea}>
|
<View style={styles.textArea}>
|
||||||
{this.leftButtons}
|
{this.leftButtons}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -172,6 +362,8 @@ export default class MessageBox extends React.Component {
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
placeholder='New Message'
|
placeholder='New Message'
|
||||||
onChangeText={text => this.onChangeText(text)}
|
onChangeText={text => this.onChangeText(text)}
|
||||||
|
onChange={event => this.onChange(event)}
|
||||||
|
value={this.state.text}
|
||||||
underlineColorAndroid='transparent'
|
underlineColorAndroid='transparent'
|
||||||
defaultValue=''
|
defaultValue=''
|
||||||
multiline
|
multiline
|
||||||
|
@ -181,6 +373,8 @@ export default class MessageBox extends React.Component {
|
||||||
{this.rightButtons}
|
{this.rightButtons}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
{this.renderMentions()}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
const MENTION_HEIGHT = 50;
|
||||||
|
|
||||||
export default StyleSheet.create({
|
export default StyleSheet.create({
|
||||||
textBox: {
|
textBox: {
|
||||||
|
@ -9,7 +10,8 @@ export default StyleSheet.create({
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#D8D8D8',
|
borderTopColor: '#D8D8D8',
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 15,
|
||||||
paddingVertical: 15
|
paddingVertical: 15,
|
||||||
|
zIndex: 2
|
||||||
},
|
},
|
||||||
safeAreaView: {
|
safeAreaView: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -62,5 +64,20 @@ export default StyleSheet.create({
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#ECECEC',
|
borderBottomColor: '#ECECEC',
|
||||||
color: '#2F343D'
|
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'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -346,8 +346,8 @@ const RocketChat = {
|
||||||
return RocketChat._sendMessageCall(message);
|
return RocketChat._sendMessageCall(message);
|
||||||
},
|
},
|
||||||
|
|
||||||
spotlight(search, usernames) {
|
spotlight(search, usernames, type) {
|
||||||
return call('spotlight', search, usernames);
|
return call('spotlight', search, usernames, type);
|
||||||
},
|
},
|
||||||
|
|
||||||
createDirectMessage(username) {
|
createDirectMessage(username) {
|
||||||
|
|
|
@ -184,6 +184,8 @@ export default class RoomView extends React.Component {
|
||||||
dataSource={this.state.dataSource}
|
dataSource={this.state.dataSource}
|
||||||
renderRow={item => this.renderItem({ item })}
|
renderRow={item => this.renderItem({ item })}
|
||||||
initialListSize={10}
|
initialListSize={10}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
keyboardDismissMode='interactive'
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
{this.renderFooter()}
|
{this.renderFooter()}
|
||||||
|
|
Loading…
Reference in New Issue