diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index ee58fd34..b727d935 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -26,8 +26,8 @@ export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
...defaultTypes,
'INIT'
]);
-export const ROOMS = createRequestTypes('ROOMS', [...defaultTypes, 'OPEN']);
-export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_TYPING', 'USER_TYPING']);
+export const ROOMS = createRequestTypes('ROOMS');
+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 MESSAGES = createRequestTypes('MESSAGES');
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [
diff --git a/app/actions/room.js b/app/actions/room.js
index 72345eeb..c98648a0 100644
--- a/app/actions/room.js
+++ b/app/actions/room.js
@@ -14,9 +14,24 @@ export function typing(data) {
...data
};
}
+
export function addUserTyping(username) {
return {
type: types.ROOM.ADD_USER_TYPING,
username
};
}
+
+export function openRoom(room) {
+ return {
+ type: types.ROOM.OPEN,
+ room
+ };
+}
+
+export function imTyping(status = true) {
+ return {
+ type: types.ROOM.IM_TYPING,
+ status
+ };
+}
diff --git a/app/actions/rooms.js b/app/actions/rooms.js
index 91c0ab3e..e1d1c8fe 100644
--- a/app/actions/rooms.js
+++ b/app/actions/rooms.js
@@ -19,10 +19,3 @@ export function roomsFailure(err) {
err
};
}
-
-export function openRoom({ rid }) {
- return {
- type: types.ROOMS.OPEN,
- rid
- };
-}
diff --git a/app/containers/MessageBox.js b/app/containers/MessageBox.js
index 7fa265aa..9eb32435 100644
--- a/app/containers/MessageBox.js
+++ b/app/containers/MessageBox.js
@@ -3,11 +3,14 @@ import PropTypes from 'prop-types';
import { View, TextInput, StyleSheet, SafeAreaView } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker';
+import { connect } from 'react-redux';
+import { imTyping } from '../actions/room';
import RocketChat from '../lib/rocketchat';
const styles = StyleSheet.create({
textBox: {
paddingTop: 1,
+ paddingHorizontal: 15,
borderTopWidth: 1,
borderTopColor: '#ccc',
backgroundColor: '#fff'
@@ -24,14 +27,19 @@ const styles = StyleSheet.create({
},
fileButton: {
color: '#aaa',
- paddingLeft: 23,
- paddingRight: 20,
paddingTop: 10,
paddingBottom: 10,
fontSize: 20
}
});
+@connect(
+ null,
+ dispatch => ({
+ typing: status => dispatch(imTyping(status))
+ })
+)
+
export default class MessageBox extends React.PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
@@ -80,7 +88,6 @@ export default class MessageBox extends React.PureComponent {
return (
-
this.component = component}
style={styles.textBoxInput}
@@ -88,9 +95,11 @@ export default class MessageBox extends React.PureComponent {
onSubmitEditing={event => this.submit(event.nativeEvent.text)}
blurOnSubmit={false}
placeholder='New message'
+ onChangeText={text => this.props.typing(text.length > 0)}
underlineColorAndroid='transparent'
defaultValue=''
/>
+
);
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 366d00d9..481c10cd 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -65,7 +65,7 @@ const RocketChat = {
Meteor.ddp.on('changed', (ddpMessage) => {
const server = { id: reduxStore.getState().server.server };
if (ddpMessage.collection === 'stream-room-messages') {
- realm.write(() => {
+ return realm.write(() => {
const message = ddpMessage.fields.args[0];
message.temp = false;
message._server = server;
@@ -78,7 +78,7 @@ const RocketChat = {
if (ev !== 'typing') {
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') {
const [type, data] = ddpMessage.fields.args;
@@ -436,8 +436,9 @@ const RocketChat = {
subscribe(...args) {
return Meteor.subscribe(...args);
},
- unsubscribe(...args) {
- return Meteor.unsubscribe(...args);
+ emitTyping(room, t = true) {
+ const { login } = reduxStore.getState();
+ return call('stream-notify-room', `${ room }/typing`, login.user.username, t);
}
};
diff --git a/app/reducers/room.js b/app/reducers/room.js
index 45971fd8..08b95b97 100644
--- a/app/reducers/room.js
+++ b/app/reducers/room.js
@@ -6,6 +6,11 @@ const initialState = {
export default function room(state = initialState, action) {
switch (action.type) {
+ case types.ROOM.OPEN:
+ return {
+ ...initialState,
+ ...action.room
+ };
case types.ROOM.ADD_USER_TYPING:
return {
...state,
@@ -16,8 +21,6 @@ export default function room(state = initialState, action) {
...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username)]
};
- // case types.LOGOUT:
- // return initialState;
default:
return state;
}
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index 876863f5..6742577e 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -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 { roomsSuccess, roomsFailure } from '../actions/rooms';
import { addUserTyping, removeUserTyping } from '../actions/room';
@@ -17,43 +18,80 @@ const watchRoomsRequest = function* watchRoomsRequest() {
yield put(roomsFailure(err.status));
}
};
-const userTyping = function* userTyping({ rid }) {
+
+const cancelTyping = function* cancelTyping(username) {
while (true) {
- const { _rid, username, typing } = yield take(types.ROOM.USER_TYPING);
- if (_rid === rid) {
- const tmp = yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username)));
+ const { typing, timeout } = yield race({
+ typing: take(types.ROOM.USER_TYPING),
+ 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);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
+
const subscriptions = [];
- yield put(messagesRequest({ rid }));
+ yield put(messagesRequest({ rid: room.rid }));
const { open } = yield race({
messages: take(types.MESSAGES.SUCCESS),
- open: take(types.ROOMS.OPEN)
+ open: take(types.ROOM.OPEN)
});
if (open) {
return;
}
- RocketChat.readMessages(rid);
- subscriptions.push(RocketChat.subscribe('stream-room-messages', rid, false));
- subscriptions.push(RocketChat.subscribe('stream-notify-room', `${ rid }/typing`, false));
- const thread = yield fork(userTyping, { rid });
- yield take(types.ROOMS.OPEN);
+
+ RocketChat.readMessages(room.rid);
+ subscriptions.push(RocketChat.subscribe('stream-room-messages', room.rid, false));
+ subscriptions.push(RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false));
+ const thread = yield fork(usersTyping, { rid: room.rid });
+ yield take(types.ROOM.OPEN);
cancel(thread);
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() {
+ yield takeLatest(types.ROOM.IM_TYPING, watchImTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
- yield takeEvery(types.ROOMS.OPEN, watchRoomOpen);
+ yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
};
export default root;
diff --git a/app/views/RoomView.js b/app/views/RoomView.js
index 8640dc95..883a48ac 100644
--- a/app/views/RoomView.js
+++ b/app/views/RoomView.js
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
-import { openRoom } from '../actions/rooms';
+import { openRoom } from '../actions/room';
import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
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 styles = StyleSheet.create({
+ typing: { fontWeight: 'bold', paddingHorizontal: 15, height: 25 },
container: {
flex: 1,
backgroundColor: '#fff'
@@ -48,6 +49,7 @@ const styles = StyleSheet.create({
@connect(
state => ({
+ username: state.login.user.username,
usersTyping: state.room.usersTyping,
server: state.server.server,
Site_Url: state.settings.Site_Url,
@@ -56,7 +58,7 @@ const styles = StyleSheet.create({
}),
dispatch => ({
actions: bindActionCreators(actions, dispatch),
- openRoom: rid => dispatch(openRoom({ rid }))
+ openRoom: room => dispatch(openRoom(room))
})
)
export default class RoomView extends React.Component {
@@ -101,7 +103,7 @@ export default class RoomView extends React.Component {
realm.objectForPrimaryKey('subscriptions', this.sid).name
});
this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
- this.props.openRoom(this.rid);
+ this.props.openRoom({ rid: this.rid });
this.data.addListener(this.updateState);
}
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 = () => {
this.setState({
@@ -190,8 +197,7 @@ export default class RoomView extends React.Component {
if (this.state.end) {
return Start of conversation;
}
- };
-
+ }
render() {
const { height } = Dimensions.get('window');
return (
@@ -208,9 +214,9 @@ export default class RoomView extends React.Component {
renderRow={item => this.renderItem({ item })}
initialListSize={10}
/>
- {this.props.usersTyping ? {this.props.usersTyping.join(',')} : null}
{this.renderFooter()}
+ {this.usersTyping}
);
}