From db0cd5abd139dbb240ede27cc6518ff950afaeed Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 7 Feb 2019 14:13:21 -0200 Subject: [PATCH] Updating room indicator (#609) Shows "Updating..." when requesting rooms from Rest API. --- app/i18n/locales/en.js | 1 + app/i18n/locales/pt-BR.js | 1 + app/lib/methods/getRooms.js | 33 +--- app/lib/methods/subscriptions/rooms.js | 18 +- app/lib/rocketchat.js | 3 +- app/reducers/rooms.js | 7 +- app/sagas/index.js | 2 + app/sagas/room.js | 156 ++++++++++++++++++ app/sagas/rooms.js | 153 ++--------------- .../RoomsListView/Header/Header.android.js | 20 ++- app/views/RoomsListView/Header/Header.ios.js | 22 ++- app/views/RoomsListView/Header/index.js | 15 +- app/views/RoomsListView/index.js | 17 +- 13 files changed, 253 insertions(+), 195 deletions(-) create mode 100644 app/sagas/room.js diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index fb7fdbd8..abd9dcfc 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -330,6 +330,7 @@ export default { Unread: 'Unread', Unread_on_top: 'Unread on top', Unstar: 'Unstar', + Updating: 'Updating...', Uploading: 'Uploading', Upload_file_question_mark: 'Upload file?', User_added_by: 'User {{userAdded}} added by {{userBy}}', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 9f0b91c9..b67439e2 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -328,6 +328,7 @@ export default { Unread: 'Não lidas', Unread_on_top: 'Não lidas no topo', Unstar: 'Remover favorito', + Updating: 'Atualizando...', Uploading: 'Subindo arquivo', Upload_file_question_mark: 'Enviar arquivo?', User_added_by: 'Usuário {{userAdded}} adicionado por {{userBy}}', diff --git a/app/lib/methods/getRooms.js b/app/lib/methods/getRooms.js index 417b3889..8e4fb7e3 100644 --- a/app/lib/methods/getRooms.js +++ b/app/lib/methods/getRooms.js @@ -1,8 +1,4 @@ -import { InteractionManager } from 'react-native'; - -import mergeSubscriptionsRooms from './helpers/mergeSubscriptionsRooms'; import database from '../realm'; -import log from '../../utils/log'; const lastMessage = () => { const message = database @@ -12,26 +8,11 @@ const lastMessage = () => { }; export default function() { - return new Promise(async(resolve, reject) => { - try { - const updatedSince = lastMessage(); - // subscriptions.get: Since RC 0.60.0 - // rooms.get: Since RC 0.62.0 - const [subscriptionsResult, roomsResult] = await (updatedSince - ? Promise.all([this.sdk.get('subscriptions.get', { updatedSince }), this.sdk.get('rooms.get', { updatedSince })]) - : Promise.all([this.sdk.get('subscriptions.get'), this.sdk.get('rooms.get')]) - ); - const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult); - - InteractionManager.runAfterInteractions(() => { - database.write(() => { - subscriptions.forEach(subscription => database.create('subscriptions', subscription, true)); - }); - resolve(subscriptions); - }); - } catch (e) { - log('getRooms', e); - reject(e); - } - }); + const updatedSince = lastMessage(); + // subscriptions.get: Since RC 0.60.0 + // rooms.get: Since RC 0.62.0 + if (updatedSince) { + return Promise.all([this.sdk.get('subscriptions.get', { updatedSince }), this.sdk.get('rooms.get', { updatedSince })]); + } + return Promise.all([this.sdk.get('subscriptions.get'), this.sdk.get('rooms.get')]); } diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js index 7f633924..494d50e7 100644 --- a/app/lib/methods/subscriptions/rooms.js +++ b/app/lib/methods/subscriptions/rooms.js @@ -4,6 +4,8 @@ import protectedFunction from '../helpers/protectedFunction'; import messagesStatus from '../../../constants/messagesStatus'; import log from '../../../utils/log'; import random from '../../../utils/random'; +import store from '../../createStore'; +import { roomsRequest } from '../../../actions/rooms'; export default async function subscribeRooms() { let timer = null; @@ -11,15 +13,11 @@ export default async function subscribeRooms() { if (timer) { return; } - timer = setTimeout(async() => { - try { - clearTimeout(timer); - timer = false; - if (this.sdk.userId) { - await this.getRooms(); - loop(); - } - } catch (e) { + timer = setTimeout(() => { + clearTimeout(timer); + timer = false; + if (this.sdk.userId) { + store.dispatch(roomsRequest()); loop(); } }, 5000); @@ -27,7 +25,7 @@ export default async function subscribeRooms() { this.sdk.onStreamData('connected', () => { if (this.sdk.userId) { - this.getRooms(); + store.dispatch(roomsRequest()); } clearTimeout(timer); timer = false; diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 6f4c4a72..bb9db20d 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -39,6 +39,7 @@ import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage' import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage'; import { getDeviceToken } from '../push'; +import { roomsRequest } from '../actions/rooms'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY'; @@ -134,7 +135,7 @@ const RocketChat = { }, loginSuccess({ user }) { reduxStore.dispatch(setUser(user)); - this.getRooms().catch(e => console.log(e)); + reduxStore.dispatch(roomsRequest()); this.subscribeRooms(); this.sdk.subscribe('activeUsers'); this.sdk.subscribe('roles'); diff --git a/app/reducers/rooms.js b/app/reducers/rooms.js index 0e16f13e..f9931bab 100644 --- a/app/reducers/rooms.js +++ b/app/reducers/rooms.js @@ -1,8 +1,9 @@ import * as types from '../actions/actionsTypes'; const initialState = { - isFetching: false, + isFetching: true, failure: false, + errorMessage: {}, searchText: '', showServerDropdown: false, closeServerDropdown: false, @@ -15,7 +16,9 @@ export default function login(state = initialState, action) { case types.ROOMS.REQUEST: return { ...state, - isFetching: true + isFetching: true, + failure: false, + errorMessage: {} }; case types.ROOMS.SUCCESS: return { diff --git a/app/sagas/index.js b/app/sagas/index.js index b85e7229..d6de1d21 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -1,6 +1,7 @@ import { all } from 'redux-saga/effects'; import login from './login'; import rooms from './rooms'; +import room from './room'; import messages from './messages'; import selectServer from './selectServer'; import createChannel from './createChannel'; @@ -14,6 +15,7 @@ const root = function* root() { init(), createChannel(), rooms(), + room(), login(), messages(), selectServer(), diff --git a/app/sagas/room.js b/app/sagas/room.js new file mode 100644 index 00000000..44849995 --- /dev/null +++ b/app/sagas/room.js @@ -0,0 +1,156 @@ +import { Alert } from 'react-native'; +import { + put, call, takeLatest, take, select, race, fork, cancel, takeEvery +} from 'redux-saga/effects'; +import { delay } from 'redux-saga'; +import EJSON from 'ejson'; + +import Navigation from '../lib/Navigation'; +import * as types from '../actions/actionsTypes'; +import { addUserTyping, removeUserTyping } from '../actions/room'; +import { messagesRequest, editCancel, replyCancel } from '../actions/messages'; +import RocketChat from '../lib/rocketchat'; +import database from '../lib/realm'; +import log from '../utils/log'; +import I18n from '../i18n'; + + +let sub; +let thread; + +const cancelTyping = function* cancelTyping(username) { + while (true) { + const { typing, timeout } = yield race({ + typing: take(types.ROOM.SOMEONE_TYPING), + timeout: call(delay, 5000) + }); + if (timeout || (typing.username === username && !typing.typing)) { + return yield put(removeUserTyping(username)); + } + } +}; + +const usersTyping = function* usersTyping({ rid }) { + while (true) { + const { _rid, username, typing } = yield take(types.ROOM.SOMEONE_TYPING); + if (_rid === rid) { + yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username))); + if (typing) { + yield fork(cancelTyping, username); + } + } + } +}; +const handleMessageReceived = function* handleMessageReceived({ message }) { + try { + const room = yield select(state => state.room); + + if (message.rid === room.rid) { + database.write(() => { + database.create('messages', EJSON.fromJSONValue(message), true); + }); + + if (room._id) { + RocketChat.readMessages(room.rid); + } + } + } catch (e) { + console.warn('handleMessageReceived', e); + } +}; + +let opened = false; + +const watchRoomOpen = function* watchRoomOpen({ room }) { + try { + if (opened) { + return; + } + opened = true; + + const auth = yield select(state => state.login.isAuthenticated); + if (!auth) { + yield take(types.LOGIN.SUCCESS); + } + + yield put(messagesRequest({ ...room })); + + if (room._id) { + RocketChat.readMessages(room.rid); + } + + sub = yield RocketChat.subscribeRoom(room); + + thread = yield fork(usersTyping, { rid: room.rid }); + yield race({ + open: take(types.ROOM.OPEN), + close: take(types.ROOM.CLOSE) + }); + opened = false; + cancel(thread); + sub.stop(); + yield put(editCancel()); + yield put(replyCancel()); + } catch (e) { + log('watchRoomOpen', e); + } +}; + +const watchuserTyping = function* watchuserTyping({ 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; + } + + try { + yield RocketChat.emitTyping(room.rid, status); + + if (status) { + yield call(delay, 5000); + yield RocketChat.emitTyping(room.rid, false); + } + } catch (e) { + log('watchuserTyping', e); + } +}; + +const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { + try { + const result = yield RocketChat.leaveRoom(rid, t); + if (result.success) { + yield Navigation.popToRoot('RoomsListView'); + } + } catch (e) { + if (e.data && e.data.errorType === 'error-you-are-last-owner') { + Alert.alert(I18n.t('Oops'), I18n.t(e.data.errorType)); + } else { + Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_room') })); + } + } +}; + +const handleEraseRoom = function* handleEraseRoom({ rid, t }) { + try { + const result = yield RocketChat.eraseRoom(rid, t); + if (result.success) { + yield Navigation.popToRoot('RoomsListView'); + } + } catch (e) { + Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('erasing_room') })); + } +}; + +const root = function* root() { + yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); + yield takeEvery(types.ROOM.OPEN, watchRoomOpen); + yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); + yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); + yield takeLatest(types.ROOM.ERASE, handleEraseRoom); +}; +export default root; diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index 6bf6347a..e8d5cb52 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -1,155 +1,28 @@ -import { Alert } from 'react-native'; -import { - put, call, takeLatest, take, select, race, fork, cancel, takeEvery -} from 'redux-saga/effects'; -import { delay } from 'redux-saga'; -import EJSON from 'ejson'; +import { put, takeLatest } from 'redux-saga/effects'; -import Navigation from '../lib/Navigation'; import * as types from '../actions/actionsTypes'; -import { addUserTyping, removeUserTyping } from '../actions/room'; -import { messagesRequest, editCancel, replyCancel } from '../actions/messages'; +import { roomsSuccess, roomsFailure } from '../actions/rooms'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import log from '../utils/log'; -import I18n from '../i18n'; +import mergeSubscriptionsRooms from '../lib/methods/helpers/mergeSubscriptionsRooms'; -let sub; -let thread; +const handleRoomsRequest = function* handleRoomsRequest() { + try { + const [subscriptionsResult, roomsResult] = yield RocketChat.getRooms(); + const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult); -const cancelTyping = function* cancelTyping(username) { - while (true) { - const { typing, timeout } = yield race({ - typing: take(types.ROOM.SOMEONE_TYPING), - timeout: call(delay, 5000) + database.write(() => { + subscriptions.forEach(subscription => database.create('subscriptions', subscription, true)); }); - if (timeout || (typing.username === username && !typing.typing)) { - return yield put(removeUserTyping(username)); - } - } -}; - -const usersTyping = function* usersTyping({ rid }) { - while (true) { - const { _rid, username, typing } = yield take(types.ROOM.SOMEONE_TYPING); - if (_rid === rid) { - yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username))); - if (typing) { - yield fork(cancelTyping, username); - } - } - } -}; -const handleMessageReceived = function* handleMessageReceived({ message }) { - try { - const room = yield select(state => state.room); - - if (message.rid === room.rid) { - database.write(() => { - database.create('messages', EJSON.fromJSONValue(message), true); - }); - - if (room._id) { - RocketChat.readMessages(room.rid); - } - } + yield put(roomsSuccess()); } catch (e) { - console.warn('handleMessageReceived', e); - } -}; - -let opened = false; - -const watchRoomOpen = function* watchRoomOpen({ room }) { - try { - if (opened) { - return; - } - opened = true; - - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - yield take(types.LOGIN.SUCCESS); - } - - yield put(messagesRequest({ ...room })); - - if (room._id) { - RocketChat.readMessages(room.rid); - } - - sub = yield RocketChat.subscribeRoom(room); - - thread = yield fork(usersTyping, { rid: room.rid }); - yield race({ - open: take(types.ROOM.OPEN), - close: take(types.ROOM.CLOSE) - }); - opened = false; - cancel(thread); - sub.stop(); - yield put(editCancel()); - yield put(replyCancel()); - } catch (e) { - log('watchRoomOpen', e); - } -}; - -const watchuserTyping = function* watchuserTyping({ 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; - } - - try { - yield RocketChat.emitTyping(room.rid, status); - - if (status) { - yield call(delay, 5000); - yield RocketChat.emitTyping(room.rid, false); - } - } catch (e) { - log('watchuserTyping', e); - } -}; - -const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) { - try { - const result = yield RocketChat.leaveRoom(rid, t); - if (result.success) { - yield Navigation.popToRoot('RoomsListView'); - } - } catch (e) { - if (e.data && e.data.errorType === 'error-you-are-last-owner') { - Alert.alert(I18n.t('Oops'), I18n.t(e.data.errorType)); - } else { - Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('leaving_room') })); - } - } -}; - -const handleEraseRoom = function* handleEraseRoom({ rid, t }) { - try { - const result = yield RocketChat.eraseRoom(rid, t); - if (result.success) { - yield Navigation.popToRoot('RoomsListView'); - } - } catch (e) { - Alert.alert(I18n.t('Oops'), I18n.t('There_was_an_error_while_action', { action: I18n.t('erasing_room') })); + yield put(roomsFailure(e)); + log('handleRoomsRequest', e); } }; const root = function* root() { - yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); - yield takeEvery(types.ROOM.OPEN, watchRoomOpen); - yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); - yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); - yield takeLatest(types.ROOM.ERASE, handleEraseRoom); + yield takeLatest(types.ROOMS.REQUEST, handleRoomsRequest); }; export default root; diff --git a/app/views/RoomsListView/Header/Header.android.js b/app/views/RoomsListView/Header/Header.android.js index 851b993e..c75d5c50 100644 --- a/app/views/RoomsListView/Header/Header.android.js +++ b/app/views/RoomsListView/Header/Header.android.js @@ -5,6 +5,8 @@ import { import PropTypes from 'prop-types'; import { TextInput } from 'react-native-gesture-handler'; +import I18n from '../../../i18n'; + const styles = StyleSheet.create({ container: { flex: 1, @@ -18,6 +20,13 @@ const styles = StyleSheet.create({ fontSize: 20, color: '#FFF' }, + serverSmall: { + fontSize: 16 + }, + updating: { + fontSize: 14, + color: '#FFF' + }, disclosure: { marginLeft: 9, marginTop: 1, @@ -30,7 +39,7 @@ const styles = StyleSheet.create({ }); const Header = ({ - onPress, serverName, showServerDropdown, setSearchInputRef, showSearchHeader, onSearchChangeText + isFetching, serverName, showServerDropdown, width, setSearchInputRef, showSearchHeader, onSearchChangeText, onPress }) => { if (showSearchHeader) { return ( @@ -46,10 +55,11 @@ const Header = ({ ); } return ( - + + {isFetching ? {I18n.t('Updating')} : null} - {serverName} + {serverName} @@ -63,7 +73,9 @@ Header.propTypes = { onPress: PropTypes.func.isRequired, onSearchChangeText: PropTypes.func.isRequired, setSearchInputRef: PropTypes.func.isRequired, - serverName: PropTypes.string + isFetching: PropTypes.bool, + serverName: PropTypes.string, + width: PropTypes.number }; Header.defaultProps = { diff --git a/app/views/RoomsListView/Header/Header.ios.js b/app/views/RoomsListView/Header/Header.ios.js index e6d52b04..5ea0dd7d 100644 --- a/app/views/RoomsListView/Header/Header.ios.js +++ b/app/views/RoomsListView/Header/Header.ios.js @@ -35,14 +35,23 @@ const styles = StyleSheet.create({ } }); -const Header = ({ onPress, serverName, showServerDropdown }) => ( +const HeaderTitle = ({ isFetching }) => { + if (isFetching) { + return {I18n.t('Updating')}; + } + return {I18n.t('Messages')}; +}; + +const Header = ({ + isFetching, serverName, showServerDropdown, onPress +}) => ( - {I18n.t('Messages')} + {serverName} @@ -52,13 +61,18 @@ const Header = ({ onPress, serverName, showServerDropdown }) => ( ); Header.propTypes = { - onPress: PropTypes.func.isRequired, + isFetching: PropTypes.bool, serverName: PropTypes.string, - showServerDropdown: PropTypes.bool.isRequired + showServerDropdown: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired }; Header.defaultProps = { serverName: 'Rocket.Chat' }; +HeaderTitle.propTypes = { + isFetching: PropTypes.bool +}; + export default Header; diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index 88a45257..d69c95d7 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -1,16 +1,19 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { responsive } from 'react-native-responsive-ui'; import { toggleServerDropdown, closeServerDropdown, closeSortDropdown, setSearch as setSearchAction } from '../../../actions/rooms'; import Header from './Header'; +@responsive @connect(state => ({ showServerDropdown: state.rooms.showServerDropdown, showSortDropdown: state.rooms.showSortDropdown, showSearchHeader: state.rooms.showSearchHeader, + isFetching: state.rooms.isFetching, serverName: state.settings.Site_Name }), dispatch => ({ close: () => dispatch(closeServerDropdown()), @@ -24,10 +27,12 @@ export default class RoomsListHeaderView extends PureComponent { showSortDropdown: PropTypes.bool, showSearchHeader: PropTypes.bool, serverName: PropTypes.string, + isFetching: PropTypes.bool, open: PropTypes.func, close: PropTypes.func, closeSort: PropTypes.func, - setSearch: PropTypes.func + setSearch: PropTypes.func, + window: PropTypes.object } componentDidUpdate(prevProps) { @@ -66,14 +71,18 @@ export default class RoomsListHeaderView extends PureComponent { render() { - const { serverName, showServerDropdown, showSearchHeader } = this.props; + const { + serverName, showServerDropdown, showSearchHeader, isFetching, window: { width } + } = this.props; return (
this.onSearchChangeText(text)} /> ); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 143a24d4..8d78aa8b 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -20,7 +20,12 @@ import I18n from '../../i18n'; import SortDropdown from './SortDropdown'; import ServerDropdown from './ServerDropdown'; import Touch from '../../utils/touch'; -import { toggleSortDropdown as toggleSortDropdownAction, openSearchHeader as openSearchHeaderAction, closeSearchHeader as closeSearchHeaderAction } from '../../actions/rooms'; +import { + toggleSortDropdown as toggleSortDropdownAction, + openSearchHeader as openSearchHeaderAction, + closeSearchHeader as closeSearchHeaderAction, + roomsRequest as roomsRequestAction +} from '../../actions/rooms'; import { appStart as appStartAction } from '../../actions'; import debounce from '../../utils/debounce'; import { isIOS, isAndroid } from '../../utils/deviceInfo'; @@ -69,7 +74,8 @@ if (isAndroid) { toggleSortDropdown: () => dispatch(toggleSortDropdownAction()), openSearchHeader: () => dispatch(openSearchHeaderAction()), closeSearchHeader: () => dispatch(closeSearchHeaderAction()), - appStart: () => dispatch(appStartAction()) + appStart: () => dispatch(appStartAction()), + roomsRequest: () => dispatch(roomsRequestAction()) })) /** @extends React.Component */ export default class RoomsListView extends LoggedView { @@ -114,7 +120,8 @@ export default class RoomsListView extends LoggedView { toggleSortDropdown: PropTypes.func, openSearchHeader: PropTypes.func, closeSearchHeader: PropTypes.func, - appStart: PropTypes.func + appStart: PropTypes.func, + roomsRequest: PropTypes.func } constructor(props) { @@ -215,7 +222,7 @@ export default class RoomsListView extends LoggedView { componentDidUpdate(prevProps) { const { - sortBy, groupByType, showFavorites, showUnread, appState + sortBy, groupByType, showFavorites, showUnread, appState, roomsRequest } = this.props; if (!( @@ -226,7 +233,7 @@ export default class RoomsListView extends LoggedView { )) { this.getSubscriptions(); } else if (appState === 'foreground' && appState !== prevProps.appState) { - RocketChat.getRooms().catch(e => console.log(e)); + roomsRequest(); } }