user typing

This commit is contained in:
Guilherme Gazzo 2017-11-21 14:55:32 -02:00
parent de25bd3bff
commit a4fbeaefff
8 changed files with 104 additions and 39 deletions

View File

@ -26,8 +26,8 @@ export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
...defaultTypes, ...defaultTypes,
'INIT' 'INIT'
]); ]);
export const ROOMS = createRequestTypes('ROOMS', [...defaultTypes, 'OPEN']); export const ROOMS = createRequestTypes('ROOMS');
export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'USER_TYPING', 'OPEN', 'IM_TYPING']);
export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES'); export const MESSAGES = createRequestTypes('MESSAGES');
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [

View File

@ -14,9 +14,24 @@ export function typing(data) {
...data ...data
}; };
} }
export function addUserTyping(username) { export function addUserTyping(username) {
return { return {
type: types.ROOM.ADD_USER_TYPING, type: types.ROOM.ADD_USER_TYPING,
username username
}; };
} }
export function openRoom(room) {
return {
type: types.ROOM.OPEN,
room
};
}
export function imTyping(status = true) {
return {
type: types.ROOM.IM_TYPING,
status
};
}

View File

@ -19,10 +19,3 @@ export function roomsFailure(err) {
err err
}; };
} }
export function openRoom({ rid }) {
return {
type: types.ROOMS.OPEN,
rid
};
}

View File

@ -3,11 +3,14 @@ import PropTypes from 'prop-types';
import { View, TextInput, StyleSheet, SafeAreaView } from 'react-native'; import { View, TextInput, StyleSheet, SafeAreaView } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker'; import ImagePicker from 'react-native-image-picker';
import { connect } from 'react-redux';
import { imTyping } from '../actions/room';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
textBox: { textBox: {
paddingTop: 1, paddingTop: 1,
paddingHorizontal: 15,
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#ccc', borderTopColor: '#ccc',
backgroundColor: '#fff' backgroundColor: '#fff'
@ -24,14 +27,19 @@ const styles = StyleSheet.create({
}, },
fileButton: { fileButton: {
color: '#aaa', color: '#aaa',
paddingLeft: 23,
paddingRight: 20,
paddingTop: 10, paddingTop: 10,
paddingBottom: 10, paddingBottom: 10,
fontSize: 20 fontSize: 20
} }
}); });
@connect(
null,
dispatch => ({
typing: status => dispatch(imTyping(status))
})
)
export default class MessageBox extends React.PureComponent { export default class MessageBox extends React.PureComponent {
static propTypes = { static propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
@ -80,7 +88,6 @@ export default class MessageBox extends React.PureComponent {
return ( return (
<View style={styles.textBox}> <View style={styles.textBox}>
<SafeAreaView style={styles.safeAreaView}> <SafeAreaView style={styles.safeAreaView}>
<Icon style={styles.fileButton} name='add-circle-outline' onPress={this.addFile} />
<TextInput <TextInput
ref={component => this.component = component} ref={component => this.component = component}
style={styles.textBoxInput} style={styles.textBoxInput}
@ -88,9 +95,11 @@ export default class MessageBox extends React.PureComponent {
onSubmitEditing={event => this.submit(event.nativeEvent.text)} onSubmitEditing={event => this.submit(event.nativeEvent.text)}
blurOnSubmit={false} blurOnSubmit={false}
placeholder='New message' placeholder='New message'
onChangeText={text => this.props.typing(text.length > 0)}
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
defaultValue='' defaultValue=''
/> />
<Icon style={styles.fileButton} name='add-circle-outline' onPress={this.addFile} />
</SafeAreaView> </SafeAreaView>
</View> </View>
); );

View File

@ -65,7 +65,7 @@ const RocketChat = {
Meteor.ddp.on('changed', (ddpMessage) => { Meteor.ddp.on('changed', (ddpMessage) => {
const server = { id: reduxStore.getState().server.server }; const server = { id: reduxStore.getState().server.server };
if (ddpMessage.collection === 'stream-room-messages') { if (ddpMessage.collection === 'stream-room-messages') {
realm.write(() => { return realm.write(() => {
const message = ddpMessage.fields.args[0]; const message = ddpMessage.fields.args[0];
message.temp = false; message.temp = false;
message._server = server; message._server = server;
@ -78,7 +78,7 @@ const RocketChat = {
if (ev !== 'typing') { if (ev !== 'typing') {
return; return;
} }
reduxStore.dispatch(typing({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); return reduxStore.dispatch(typing({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
} }
if (ddpMessage.collection === 'stream-notify-user') { if (ddpMessage.collection === 'stream-notify-user') {
const [type, data] = ddpMessage.fields.args; const [type, data] = ddpMessage.fields.args;
@ -436,8 +436,9 @@ const RocketChat = {
subscribe(...args) { subscribe(...args) {
return Meteor.subscribe(...args); return Meteor.subscribe(...args);
}, },
unsubscribe(...args) { emitTyping(room, t = true) {
return Meteor.unsubscribe(...args); const { login } = reduxStore.getState();
return call('stream-notify-room', `${ room }/typing`, login.user.username, t);
} }
}; };

View File

@ -6,6 +6,11 @@ const initialState = {
export default function room(state = initialState, action) { export default function room(state = initialState, action) {
switch (action.type) { switch (action.type) {
case types.ROOM.OPEN:
return {
...initialState,
...action.room
};
case types.ROOM.ADD_USER_TYPING: case types.ROOM.ADD_USER_TYPING:
return { return {
...state, ...state,
@ -16,8 +21,6 @@ export default function room(state = initialState, action) {
...state, ...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username)] usersTyping: [...state.usersTyping.filter(user => user !== action.username)]
}; };
// case types.LOGOUT:
// return initialState;
default: default:
return state; return state;
} }

View File

@ -1,4 +1,5 @@
import { put, call, takeLatest, takeEvery, take, select, race, fork, cancel } from 'redux-saga/effects'; import { put, call, takeLatest, take, select, race, fork, cancel } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { roomsSuccess, roomsFailure } from '../actions/rooms'; import { roomsSuccess, roomsFailure } from '../actions/rooms';
import { addUserTyping, removeUserTyping } from '../actions/room'; import { addUserTyping, removeUserTyping } from '../actions/room';
@ -17,43 +18,80 @@ const watchRoomsRequest = function* watchRoomsRequest() {
yield put(roomsFailure(err.status)); yield put(roomsFailure(err.status));
} }
}; };
const userTyping = function* userTyping({ rid }) {
const cancelTyping = function* cancelTyping(username) {
while (true) { while (true) {
const { _rid, username, typing } = yield take(types.ROOM.USER_TYPING); const { typing, timeout } = yield race({
if (_rid === rid) { typing: take(types.ROOM.USER_TYPING),
const tmp = yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username))); timeout: yield call(delay, 5000)
});
if (timeout || (typing.username === username && !typing.typing)) {
return yield put(removeUserTyping(username));
} }
} }
}; };
const watchRoomOpen = function* watchRoomOpen({ rid }) { const usersTyping = function* usersTyping({ rid }) {
while (true) {
const { _rid, username, typing } = yield take(types.ROOM.USER_TYPING);
if (_rid === rid) {
yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username)));
if (typing) {
fork(cancelTyping, username);
}
}
}
};
const watchRoomOpen = function* watchRoomOpen({ room }) {
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
if (!auth) { if (!auth) {
yield take(types.LOGIN.SUCCESS); yield take(types.LOGIN.SUCCESS);
} }
const subscriptions = []; const subscriptions = [];
yield put(messagesRequest({ rid })); yield put(messagesRequest({ rid: room.rid }));
const { open } = yield race({ const { open } = yield race({
messages: take(types.MESSAGES.SUCCESS), messages: take(types.MESSAGES.SUCCESS),
open: take(types.ROOMS.OPEN) open: take(types.ROOM.OPEN)
}); });
if (open) { if (open) {
return; return;
} }
RocketChat.readMessages(rid);
subscriptions.push(RocketChat.subscribe('stream-room-messages', rid, false)); RocketChat.readMessages(room.rid);
subscriptions.push(RocketChat.subscribe('stream-notify-room', `${ rid }/typing`, false)); subscriptions.push(RocketChat.subscribe('stream-room-messages', room.rid, false));
const thread = yield fork(userTyping, { rid }); subscriptions.push(RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false));
yield take(types.ROOMS.OPEN); const thread = yield fork(usersTyping, { rid: room.rid });
yield take(types.ROOM.OPEN);
cancel(thread); cancel(thread);
subscriptions.forEach(sub => sub.stop()); subscriptions.forEach(sub => sub.stop());
}; };
const watchImTyping = function* watchImTyping({ status }) {
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
const room = yield select(state => state.room);
if (!room) {
return;
}
yield RocketChat.emitTyping(room.rid, status);
if (status) {
yield call(delay, 5000);
yield RocketChat.emitTyping(room.rid, false);
}
};
const root = function* root() { const root = function* root() {
yield takeLatest(types.ROOM.IM_TYPING, watchImTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest); yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
yield takeEvery(types.ROOMS.OPEN, watchRoomOpen); yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
}; };
export default root; export default root;

View File

@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as actions from '../actions'; import * as actions from '../actions';
import { openRoom } from '../actions/rooms'; import { openRoom } from '../actions/room';
import realm from '../lib/realm'; import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import Message from '../containers/Message'; import Message from '../containers/Message';
@ -15,6 +15,7 @@ import KeyboardView from '../presentation/KeyboardView';
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id }); const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
const styles = StyleSheet.create({ const styles = StyleSheet.create({
typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 },
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#fff' backgroundColor: '#fff'
@ -48,6 +49,7 @@ const styles = StyleSheet.create({
@connect( @connect(
state => ({ state => ({
username: state.login.user.username,
usersTyping: state.room.usersTyping, usersTyping: state.room.usersTyping,
server: state.server.server, server: state.server.server,
Site_Url: state.settings.Site_Url, Site_Url: state.settings.Site_Url,
@ -56,7 +58,7 @@ const styles = StyleSheet.create({
}), }),
dispatch => ({ dispatch => ({
actions: bindActionCreators(actions, dispatch), actions: bindActionCreators(actions, dispatch),
openRoom: rid => dispatch(openRoom({ rid })) openRoom: room => dispatch(openRoom(room))
}) })
) )
export default class RoomView extends React.Component { export default class RoomView extends React.Component {
@ -101,7 +103,7 @@ export default class RoomView extends React.Component {
realm.objectForPrimaryKey('subscriptions', this.sid).name realm.objectForPrimaryKey('subscriptions', this.sid).name
}); });
this.timer = setTimeout(() => this.setState({ slow: true }), 5000); this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
this.props.openRoom(this.rid); this.props.openRoom({ rid: this.rid });
this.data.addListener(this.updateState); this.data.addListener(this.updateState);
} }
componentDidMount() { componentDidMount() {
@ -135,7 +137,12 @@ export default class RoomView extends React.Component {
}); });
}); });
} }
}; }
get usersTyping() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username);
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : null;
}
updateState = () => { updateState = () => {
this.setState({ this.setState({
@ -190,8 +197,7 @@ export default class RoomView extends React.Component {
if (this.state.end) { if (this.state.end) {
return <Text style={styles.loadingMore}>Start of conversation</Text>; return <Text style={styles.loadingMore}>Start of conversation</Text>;
} }
}; }
render() { render() {
const { height } = Dimensions.get('window'); const { height } = Dimensions.get('window');
return ( return (
@ -208,9 +214,9 @@ export default class RoomView extends React.Component {
renderRow={item => this.renderItem({ item })} renderRow={item => this.renderItem({ item })}
initialListSize={10} initialListSize={10}
/> />
{this.props.usersTyping ? <Text>{this.props.usersTyping.join(',')}</Text> : null}
</SafeAreaView> </SafeAreaView>
{this.renderFooter()} {this.renderFooter()}
<Text style={styles.typing}>{this.usersTyping}</Text>
</KeyboardView> </KeyboardView>
); );
} }