Init Redux usage

This commit is contained in:
Rodrigo Nascimento 2017-08-12 22:35:09 -03:00
parent 2626e4a7bb
commit 37d34b0b1f
18 changed files with 397 additions and 103 deletions

View File

@ -1,3 +1,4 @@
{
"presets": ["react-native"]
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
}

View File

@ -115,5 +115,7 @@
"object-shorthand": 2,
"consistent-return": 0
},
"globals": {}
"globals": {
"__DEV__": true
}
}

131
app/actions/index.js Normal file
View File

@ -0,0 +1,131 @@
import * as types from '../constants/types';
export function setCurrentServer(server) {
return {
type: types.SET_CURRENT_SERVER,
payload: server
};
}
export function preventESLintError() {
return {};
}
// // GENRES
// export function retrieveMoviesGenresSuccess(res) {
// return {
// type: types.RETRIEVE_MOVIES_GENRES_SUCCESS,
// moviesGenres: res.data
// };
// }
// export function retrieveMoviesGenres() {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/genre/movie/list?api_key=${TMDB_API_KEY}`)
// .then(res => {
// dispatch(retrieveMoviesGenresSuccess(res));
// })
// .catch(error => {
// console.log(error); //eslint-disable-line
// });
// };
// }
// // POPULAR
// export function retrievePopularMoviesSuccess(res) {
// return {
// type: types.RETRIEVE_POPULAR_MOVIES_SUCCESS,
// popularMovies: res.data
// };
// }
// export function retrievePopularMovies(page) {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/movie/popular?api_key=${TMDB_API_KEY}&page=${page}`)
// .then(res => {
// dispatch(retrievePopularMoviesSuccess(res));
// })
// .catch(error => {
// console.log('Popular', error); //eslint-disable-line
// });
// };
// }
// // NOW PLAYING
// export function retrieveNowPlayingMoviesSuccess(res) {
// return {
// type: types.RETRIEVE_NOWPLAYING_MOVIES_SUCCESS,
// nowPlayingMovies: res.data
// };
// }
// export function retrieveNowPlayingMovies(page) {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/movie/now_playing?api_key=${TMDB_API_KEY}&page=${page}`)
// .then(res => {
// dispatch(retrieveNowPlayingMoviesSuccess(res));
// })
// .catch(error => {
// console.log('Now Playing', error); //eslint-disable-line
// });
// };
// }
// // MOVIES LIST
// export function retrieveMoviesListSuccess(res) {
// return {
// type: types.RETRIEVE_MOVIES_LIST_SUCCESS,
// list: res.data
// };
// }
// export function retrieveMoviesList(type, page) {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/movie/${type}?api_key=${TMDB_API_KEY}&page=${page}`)
// .then(res => {
// dispatch(retrieveMoviesListSuccess(res));
// })
// .catch(error => {
// console.log('Movies List', error); //eslint-disable-line
// });
// };
// }
// // SEARCH RESULTS
// export function retrieveMoviesSearchResultsSuccess(res) {
// return {
// type: types.RETRIEVE_MOVIES_SEARCH_RESULT_SUCCESS,
// searchResults: res.data
// };
// }
// export function retrieveMoviesSearchResults(query, page) {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/search/movie?api_key=${TMDB_API_KEY}&query=${query}&page=${page}`)
// .then(res => {
// dispatch(retrieveMoviesSearchResultsSuccess(res));
// })
// .catch(error => {
// console.log('Movies Search Results', error); //eslint-disable-line
// });
// };
// }
// // MOVIE DETAILS
// export function retrieveMovieDetailsSuccess(res) {
// return {
// type: types.RETRIEVE_MOVIE_DETAILS_SUCCESS,
// details: res.data
// };
// }
// export function retrieveMovieDetails(movieId) {
// return function (dispatch) {
// return axios.get(`${TMDB_URL}/movie/${movieId}?api_key=${TMDB_API_KEY}&append_to_response=casts,images,videos`)
// .then(res => {
// dispatch(retrieveMovieDetailsSuccess(res));
// })
// .catch(error => {
// console.log('Movie Details', error); //eslint-disable-line
// });
// };
// }

10
app/constants/types.js Normal file
View File

@ -0,0 +1,10 @@
// export const RETRIEVE_MOVIES_GENRES_SUCCESS = 'RETRIEVE_MOVIES_GENRES_SUCCESS';
// export const RETRIEVE_POPULAR_MOVIES_SUCCESS = 'RETRIEVE_POPULAR_MOVIES_SUCCESS';
// export const RETRIEVE_NOWPLAYING_MOVIES_SUCCESS = 'RETRIEVE_NOWPLAYING_MOVIES_SUCCESS';
// export const RETRIEVE_MOVIES_LIST_SUCCESS = 'RETRIEVE_MOVIES_LIST_SUCCESS';
// export const RETRIEVE_MOVIE_DETAILS_SUCCESS = 'RETRIEVE_MOVIE_DETAILS_SUCCESS';
// export const RETRIEVE_MOVIES_SEARCH_RESULT_SUCCESS = 'RETRIEVE_MOVIES_SEARCH_RESULT_SUCCESS';
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const ESLINT_FIX = 'ESLINT_FIX';

22
app/lib/createStore.js Normal file
View File

@ -0,0 +1,22 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import rootReducer from '../reducers/rootReducer';
let middleware = [thunk];
if (__DEV__) {
/* eslint-disable global-require */
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
middleware = [...middleware, reduxImmutableStateInvariant, logger];
} else {
middleware = [...middleware];
}
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(...middleware)
);
}

View File

@ -70,20 +70,15 @@ const messagesSchema = {
}
};
// Realm.clearTestState();
const realm = new Realm({
schema: [settingsSchema, serversSchema, subscriptionSchema, messagesSchema, usersSchema]
});
export default realm;
// Clear settings
// Realm.clearTestState();
// realm.write(() => {
// // const allSettins = realm.objects('settings');
// // realm.delete(allSettins);
//
// // realm.create('servers', { id: 'https://demo.rocket.chat', current: false }, true);
// // realm.create('servers', { id: 'http://localhost:3000', current: false }, true);
// realm.create('servers', { id: 'https://demo.rocket.chat', current: false }, true);
// realm.create('servers', { id: 'http://localhost:3000', current: false }, true);
// realm.create('servers', { id: 'http://10.0.2.2:3000', current: false }, true);
// });

View File

@ -1,5 +1,7 @@
import Meteor from 'react-native-meteor';
import Random from 'react-native-meteor/lib/Random';
import { AsyncStorage } from 'react-native';
import realm from './realm';
import debounce from '../utils/debounce';
@ -32,13 +34,14 @@ const write = (() => {
run();
};
})();
const RocketChat = {
const RocketChat = {
createChannel({ name, users, type }) {
return new Promise((resolve, reject) => {
Meteor.call(type ? 'createChannel' : 'createPrivateGroup', name, users, type, (err, res) => (err ? reject(err) : resolve(res)));
});
},
get currentServer() {
const current = realm.objects('servers').filtered('current = true').slice(0, 1)[0];
return current && current.id;
@ -51,6 +54,15 @@ const RocketChat = {
});
},
async getUserToken() {
const TOKEN_KEY = 'reactnativemeteor_usertoken';
try {
return await AsyncStorage.getItem(TOKEN_KEY);
} catch (error) {
console.warn(`AsyncStorage error: ${ error.message }`);
}
},
connect(cb) {
const url = `${ RocketChat.currentServer }/websocket`;
@ -94,6 +106,7 @@ const RocketChat = {
realm.create('messages', message, true);
});
}
this.subCache = this.subCache || {};
this.roomCache = this.roomCache || {};
this.cache = {};
@ -319,6 +332,7 @@ const RocketChat = {
};
export default RocketChat;
Meteor.Accounts.onLogin(() => {
Promise.all([call('subscriptions/get'), call('rooms/get')]).then(([subscriptions, rooms]) => {
subscriptions = subscriptions.sort((s1, s2) => (s1.rid > s2.rid ? 1 : -1));

View File

@ -1,18 +1,22 @@
import { Navigation } from 'react-native-navigation';
import { Provider } from 'react-redux';
import LoginView from './views/login';
import NewServerView from './views/serverNew';
import ListServerView from './views/serverList';
import RoomsListView from './views/roomsList';
import RoomView from './views/room';
import CreateChannel from './views/CreateChannel';
import configureStore from './lib/createStore';
const store = configureStore();
Navigation.registerComponent('Rooms', () => RoomsListView);
Navigation.registerComponent('Room', () => RoomView);
Navigation.registerComponent('ListServer', () => ListServerView);
Navigation.registerComponent('Login', () => LoginView);
Navigation.registerComponent('NewServer', () => NewServerView);
Navigation.registerComponent('CreateChannel', () => CreateChannel);
Navigation.registerComponent('Rooms', () => RoomsListView, store, Provider);
Navigation.registerComponent('Room', () => RoomView, store, Provider);
Navigation.registerComponent('ListServer', () => ListServerView, store, Provider);
Navigation.registerComponent('Login', () => LoginView, store, Provider);
Navigation.registerComponent('NewServer', () => NewServerView, store, Provider);
Navigation.registerComponent('CreateChannel', () => CreateChannel, store, Provider);
Navigation.startSingleScreenApp({
screen: {

View File

@ -0,0 +1,6 @@
import RocketChat from '../lib/rocketchat';
export default {
server: RocketChat.currentServer,
login: {}
};

52
app/reducers/reducers.js Normal file
View File

@ -0,0 +1,52 @@
import RocketChat from '../lib/rocketchat';
import * as types from '../constants/types';
import initialState from './initialState';
export default function(state = initialState, action) {
switch (action.type) {
case types.SET_CURRENT_SERVER:
RocketChat.currentServer = action.payload;
return {
...state,
server: action.payload
};
// case types.RETRIEVE_POPULAR_MOVIES_SUCCESS:
// return {
// ...state,
// popularMovies: action.popularMovies
// };
// case types.RETRIEVE_NOWPLAYING_MOVIES_SUCCESS:
// return {
// ...state,
// nowPlayingMovies: action.nowPlayingMovies
// };
// case types.RETRIEVE_MOVIES_GENRES_SUCCESS:
// return {
// ...state,
// genres: action.moviesGenres
// };
// case types.RETRIEVE_MOVIES_LIST_SUCCESS:
// return {
// ...state,
// list: action.list
// };
// case types.RETRIEVE_MOVIE_DETAILS_SUCCESS:
// return {
// ...state,
// details: action.details
// };
// case types.RETRIEVE_MOVIES_SEARCH_RESULT_SUCCESS:
// return {
// ...state,
// searchResults: action.searchResults
// };
default:
return state;
}
}

View File

@ -0,0 +1,9 @@
// import { combineReducers } from 'redux';
import reducers from './reducers';
// const rootReducer = combineReducers({
// reducers
// });
// export default rootReducer;
export default reducers;

View File

@ -1,8 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TextInput, StyleSheet } from 'react-native';
import RocketChat from '../lib/rocketchat';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import RocketChat from '../lib/rocketchat';
import KeyboardView from '../components/KeyboardView';
const styles = StyleSheet.create({
@ -25,9 +28,15 @@ const styles = StyleSheet.create({
}
});
@connect(state => ({
server: state.server
}), dispatch => ({
actions: bindActionCreators(actions, dispatch)
}))
export default class LoginView extends React.Component {
static propTypes = {
navigator: PropTypes.object.isRequired
navigator: PropTypes.object.isRequired,
server: PropTypes.string.isRequired
}
static navigationOptions = () => ({
@ -45,9 +54,11 @@ export default class LoginView extends React.Component {
this.props.navigator.setTitle({
title: 'Login'
});
}
componentWillReceiveProps(nextProps) {
this.props.navigator.setSubTitle({
subtitle: RocketChat.currentServer
subtitle: nextProps.server
});
}

View File

@ -2,6 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Text, View, StyleSheet, Button } from 'react-native';
import { ListView } from 'realm/react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions';
import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import debounce from '../utils/throttle';
@ -37,12 +41,19 @@ const styles = StyleSheet.create({
}
});
@connect(state => ({
server: state.server
}), dispatch => ({
actions: bindActionCreators(actions, dispatch)
}))
export default class RoomView extends React.Component {
static propTypes = {
navigator: PropTypes.object.isRequired,
rid: PropTypes.string,
sid: PropTypes.string,
name: PropTypes.string
name: PropTypes.string,
server: PropTypes.string
}
constructor(props) {
@ -50,7 +61,7 @@ export default class RoomView extends React.Component {
this.rid = props.rid || realm.objectForPrimaryKey('subscriptions', props.sid).rid;
// this.rid = 'GENERAL';
this.data = realm.objects('messages').filtered('_server.id = $0 AND rid = $1', RocketChat.currentServer, this.rid).sorted('ts', true);
this.data = realm.objects('messages').filtered('_server.id = $0 AND rid = $1', this.props.server, this.rid).sorted('ts', true);
this.state = {
dataSource: ds.cloneWithRows(this.data.slice(0, 10)),
loaded: true,

View File

@ -4,8 +4,12 @@ import { ListView } from 'realm/react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Text, View, StyleSheet, TouchableOpacity, Platform, TextInput } from 'react-native';
import { Text, View, StyleSheet, TouchableOpacity, Platform, TextInput } from 'react-native';
import Meteor from 'react-native-meteor';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from '../actions';
import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import RoomItem from '../components/RoomItem';
@ -59,24 +63,17 @@ const styles = StyleSheet.create({
}
});
let setInitialData;
Meteor.getData().on('loggingIn', () => {
setTimeout(() => {
if (Meteor._isLoggingIn === false && Meteor.userId() == null) {
console.log('loggingIn', Meteor.userId());
Navigation.showModal({
screen: 'Login',
animationType: 'slide-up'
});
}
}, 100);
});
Meteor.Accounts.onLogin(() => {
console.log('onLogin');
});
Meteor.Accounts.onLoginFailure(() => {
Navigation.showModal({
screen: 'Login',
animationType: 'slide-up'
});
});
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
class RoomsListItem extends React.PureComponent {
static propTypes = {
@ -99,30 +96,21 @@ class RoomsListItem extends React.PureComponent {
);
}
}
@connect(state => ({
server: state.server
}), dispatch => ({
actions: bindActionCreators(actions, dispatch)
}))
export default class RoomsListView extends React.Component {
static propTypes = {
navigator: PropTypes.object.isRequired
}
static navigationOptions = (props) => {
const server = RocketChat.currentServer ? RocketChat.currentServer.replace(/^https?:\/\//, '') : '';
const textAlign = Platform.OS === 'ios' ? 'center' : 'left';
const marginLeft = Platform.OS === 'ios' ? 0 : 20;
const position = Platform.OS === 'ios' ? 'headerLeft' : 'headerRight';
return {
headerTitle: <View style={{ height: 10, width: 200, top: -10, marginLeft }}>
<Text style={{ textAlign, fontSize: 16, fontWeight: '600' }}>Channels</Text>
<Text style={{ textAlign, fontSize: 10 }}>{server}</Text>
</View>,
title: 'Channels',
[position]: <Button title='Servers' onPress={() => props.navigation.navigate('ListServerModal')} />
};
navigator: PropTypes.object.isRequired,
server: PropTypes.string
}
constructor(props) {
super(props);
this.data = realm.objects('subscriptions').filtered('_server.id = $0', RocketChat.currentServer).sorted('_updatedAt', true);
this.data = realm.objects('subscriptions').filtered('_server.id = $0', this.props.server).sorted('_updatedAt', true);
this.state = {
dataSource: ds.cloneWithRows(this.data.sorted('_updatedAt', true).slice(0, 10)),
searching: false,
@ -134,26 +122,17 @@ export default class RoomsListView extends React.Component {
}
componentWillMount() {
setInitialData = this.setInitialData;
if (RocketChat.currentServer) {
this.props.navigator.setSubTitle({
subtitle: RocketChat.currentServer
});
}
const button = Platform.OS === 'ios' ? 'leftButtons' : 'rightButtons';
this.props.navigator.setButtons({
leftButtons: [{
[button]: [{
id: 'servers',
title: 'Servers'
}],
// rightButtons: [], // see "Adding buttons to the navigator" below for format (optional)
animated: true
});
// this.setInitialData();
if (RocketChat.currentServer) {
RocketChat.connect();
if (this.props.server) {
this.setInitialData();
} else {
Navigation.showModal({
screen: 'ListServer',
@ -165,6 +144,14 @@ export default class RoomsListView extends React.Component {
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.server !== this.props.server) {
this.setInitialData(nextProps);
}
}
componentWillUpdate
componentWillUnmount() {
this.data.removeListener(this.updateState);
}
@ -194,7 +181,7 @@ export default class RoomsListView extends React.Component {
if (searchText !== '') {
const dataSource = [];
const usernames = [];
realm.objects('subscriptions').filtered('_server.id = $0 AND name CONTAINS[c] $1', RocketChat.currentServer, searchText).forEach((sub) => {
realm.objects('subscriptions').filtered('_server.id = $0 AND name CONTAINS[c] $1', this.props.server, searchText).forEach((sub) => {
dataSource.push(sub);
if (sub.t === 'd') {
@ -228,21 +215,36 @@ export default class RoomsListView extends React.Component {
}
}
}
setInitialData = () => {
if (this.data) {
this.data.removeListener(this.updateState);
}
this.data = realm.objects('subscriptions').filtered('_server.id = $0', RocketChat.currentServer).sorted('_updatedAt', true);
this.data.addListener(this.updateState);
this.updateState();
setInitialData = (props = this.props) => {
props.navigator.setSubTitle({
subtitle: props.server
});
RocketChat.getUserToken().then((token) => {
if (!token) {
Navigation.showModal({
screen: 'Login',
animationType: 'slide-up'
});
}
RocketChat.connect();
if (this.data) {
this.data.removeListener(this.updateState);
}
this.data = realm.objects('subscriptions').filtered('_server.id = $0', props.server).sorted('_updatedAt', true);
this.data.addListener(this.updateState);
this.updateState();
});
}
getSubscriptions = () => this.data.sorted('_updatedAt', true)
updateState = debounce(() => {
this.setState({
dataSource: ds.cloneWithRows(this.data.filtered('_server.id = $0', RocketChat.currentServer))
dataSource: ds.cloneWithRows(this.data.filtered('_server.id = $0', this.props.server))
});
}, 500);
@ -266,7 +268,7 @@ export default class RoomsListView extends React.Component {
if (item.search) {
if (item.t === 'd') {
RocketChat.createDirectMessage(item.username)
.then(room => realm.objects('subscriptions').filtered('_server.id = $0 AND rid = $1', RocketChat.currentServer, room.rid))
.then(room => realm.objects('subscriptions').filtered('_server.id = $0 AND rid = $1', this.props.server, room.rid))
.then(subs => navigateToRoom({ sid: subs[0]._id }))
.then(() => clearSearch());
} else {

View File

@ -1,11 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navigation } from 'react-native-navigation';
import { bindActionCreators } from 'redux';
import Zeroconf from 'react-native-zeroconf';
import { View, Text, SectionList, StyleSheet } from 'react-native';
import { View, Text, SectionList, Platform, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import * as actions from '../actions';
import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
const styles = StyleSheet.create({
view: {
@ -49,9 +51,17 @@ const styles = StyleSheet.create({
const zeroconf = new Zeroconf();
@connect(state => ({
server: state.server
}), dispatch => ({
actions: bindActionCreators(actions, dispatch)
}))
export default class ListServerView extends React.Component {
static propTypes = {
navigator: PropTypes.object.isRequired
navigator: PropTypes.object.isRequired,
actions: PropTypes.object,
server: PropTypes.string
}
constructor(props) {
@ -59,15 +69,6 @@ export default class ListServerView extends React.Component {
this.state = {
sections: []
};
}
componentWillMount() {
realm.addListener('change', this.updateState);
zeroconf.on('update', this.updateState);
zeroconf.scan('http', 'tcp', 'local.');
this.state = this.getState();
this.props.navigator.setTitle({
title: 'Servers'
@ -78,12 +79,29 @@ export default class ListServerView extends React.Component {
id: 'add',
title: 'Add'
}],
leftButtons: props.server && Platform.select({
ios: [{
id: 'close',
title: 'Close'
}]
}),
animated: true
});
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
componentWillMount() {
realm.addListener('change', this.updateState);
zeroconf.on('update', this.updateState);
zeroconf.scan('http', 'tcp', 'local.');
this.state = this.getState();
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
componentWillUnmount() {
zeroconf.stop();
realm.removeListener('change', this.updateState);
@ -105,18 +123,20 @@ export default class ListServerView extends React.Component {
});
}
}
if (event.id === 'didDisappear' && this.state.server) {
this.props.actions.setCurrentServer(this.state.server);
}
}
onPressItem = (item) => {
RocketChat.currentServer = item.id;
RocketChat.connect();
Navigation.dismissModal({
animationType: 'slide-down'
});
// this.props.navigation.state.params.onSelect();
// this.props.navigation.dispatch({ type: 'Navigation/BACK' });
this.setState({
server: item.id
});
}
getState = () => {

View File

@ -4,8 +4,6 @@ import { Navigation } from 'react-native-navigation';
import { Text, TextInput, View, StyleSheet } from 'react-native';
import _ from 'underscore';
import RocketChat from '../lib/rocketchat';
import KeyboardView from '../components/KeyboardView';
const styles = StyleSheet.create({
@ -49,6 +47,7 @@ const styles = StyleSheet.create({
}
});
export default class NewServerView extends React.Component {
static propTypes = {
navigator: PropTypes.object.isRequired
@ -80,7 +79,6 @@ export default class NewServerView extends React.Component {
this.inputElement.blur();
this.validateServer(url).then(() => {
RocketChat.currentServer = url;
Navigation.dismissModal({
animationType: 'slide-down'
});

View File

@ -1,9 +1,9 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true
},
"exclude": [
"node_modules"
]
}
}

View File

@ -11,6 +11,7 @@
"android": "react-native run-android"
},
"dependencies": {
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"prop-types": "^15.5.10",
"react": "16.0.0-alpha.12",
"react-emojione": "^3.1.10",
@ -33,7 +34,12 @@
"react-native-vector-icons": "^4.3.0",
"react-native-zeroconf": "^0.8.1",
"react-navigation": "^1.0.0-beta.11",
"react-redux": "^5.0.6",
"realm": "^1.10.1",
"redux": "^3.7.2",
"redux-immutable-state-invariant": "^2.0.0",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"strip-ansi": "^4.0.0",
"underscore": "^1.8.3"
},