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 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(<MyIcon
|
||||
style={[styles.actionButtons, { color: '#1D74F5' }]}
|
||||
name='send'
|
||||
key='sendIcon'
|
||||
accessibilityLabel='Send message'
|
||||
accessibilityTraits='button'
|
||||
onPress={() => this.submit(this.component._lastNativeText)}
|
||||
onPress={() => this.submit(this.state.text)}
|
||||
/>);
|
||||
}
|
||||
icons.push(<MyIcon
|
||||
|
@ -100,12 +139,10 @@ export default class MessageBox extends React.Component {
|
|||
return icons;
|
||||
}
|
||||
|
||||
// get placeholder() {
|
||||
// return `New Message`.substring(0, 35);
|
||||
// }
|
||||
updateSize = (height) => {
|
||||
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,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() {
|
||||
const { height } = this.state;
|
||||
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}>
|
||||
{this.leftButtons}
|
||||
<TextInput
|
||||
|
@ -172,6 +362,8 @@ export default class MessageBox extends React.Component {
|
|||
blurOnSubmit={false}
|
||||
placeholder='New Message'
|
||||
onChangeText={text => this.onChangeText(text)}
|
||||
onChange={event => this.onChange(event)}
|
||||
value={this.state.text}
|
||||
underlineColorAndroid='transparent'
|
||||
defaultValue=''
|
||||
multiline
|
||||
|
@ -181,6 +373,8 @@ export default class MessageBox extends React.Component {
|
|||
{this.rightButtons}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
{this.renderMentions()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
/>
|
||||
</SafeAreaView>
|
||||
{this.renderFooter()}
|
||||
|
|
Loading…
Reference in New Issue