Create room (#42)

* Added select users view

* create room working

* - Show photo on avatar

* Switched state for redux

* Navigating to created room
This commit is contained in:
Diego Mello 2017-09-25 10:15:28 -03:00 committed by Guilherme Gazzo
parent d55db0fca5
commit e0777a969e
12 changed files with 1081 additions and 4950 deletions

View File

@ -1,11 +1,10 @@
const REQUEST = 'REQUEST';
const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base, types = defaultTypes) {
const res = {};
types.forEach(type => res[type] = `${ base }_${ type }`);
types.forEach(type => (res[type] = `${ base }_${ type }`));
return res;
}
@ -14,7 +13,16 @@ export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_TOKEN',
export const ROOMS = createRequestTypes('ROOMS');
export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES');
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes, 'REQUEST_USERS', 'SUCCESS_USERS', 'FAILURE_USERS', 'SET_USERS']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [
...defaultTypes,
'REQUEST_USERS',
'SUCCESS_USERS',
'FAILURE_USERS',
'SET_USERS',
'ADD_USER',
'REMOVE_USER',
'RESET'
]);
export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']);
export const SERVER = createRequestTypes('SERVER', [...defaultTypes, 'SELECT', 'CHANGED', 'ADD']);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);

View File

@ -21,7 +21,6 @@ export function createChannelFailure(err) {
};
}
export function createChannelRequestUsers(data) {
return {
type: types.CREATE_CHANNEL.REQUEST_USERS,
@ -49,3 +48,23 @@ export function createChannelFailureUsers(err) {
err
};
}
export function addUser(user) {
return {
type: types.CREATE_CHANNEL.ADD_USER,
user
};
}
export function removeUser(user) {
return {
type: types.CREATE_CHANNEL.REMOVE_USER,
user
};
}
export function reset() {
return {
type: types.CREATE_CHANNEL.RESET
};
}

View File

@ -1,5 +1,6 @@
import React from 'react';
import { StackNavigator, DrawerNavigator } from 'react-navigation';
import { Button } from 'react-native';
import { StackNavigator, DrawerNavigator, NavigationActions } from 'react-navigation';
// import { Platform } from 'react-native';
import Sidebar from '../../containers/Sidebar';
@ -8,10 +9,18 @@ import DrawerMenuButton from '../../presentation/DrawerMenuButton';
import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView';
import CreateChannelView from '../../views/CreateChannelView';
import SelectUsersView from '../../views/SelectUsersView';
const drawerPosition = 'left';
const drawerIconPosition = 'headerLeft';
const backToScreen = (navigation, routeName) => {
const action = NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName })]
});
navigation.dispatch(action);
};
const AuthRoutes = StackNavigator(
{
@ -20,7 +29,7 @@ const AuthRoutes = StackNavigator(
navigationOptions({ navigation }) {
return {
title: 'Rooms',
[drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)
[drawerIconPosition]: <DrawerMenuButton navigation={navigation} />
};
}
},
@ -28,7 +37,10 @@ const AuthRoutes = StackNavigator(
screen: RoomView,
navigationOptions({ navigation }) {
return {
title: navigation.state.params.title || 'Room'
title: navigation.state.params.title || 'Room',
headerLeft: (
<Button title={'Back'} onPress={() => backToScreen(navigation, 'RoomsList')} />
)
// [drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)÷
};
}
@ -38,25 +50,33 @@ const AuthRoutes = StackNavigator(
navigationOptions: {
title: 'Create Channel'
}
},
SelectUsers: {
screen: SelectUsersView,
navigationOptions: {
title: 'Select Users'
}
}
},
{
}
{}
);
const Routes = DrawerNavigator({
const Routes = DrawerNavigator(
{
Home: {
screen: AuthRoutes,
navigationOptions({ navigation }) {
return {
title: 'Rooms',
[drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)
[drawerIconPosition]: <DrawerMenuButton navigation={navigation} />
};
}
}
}, {
},
{
contentComponent: Sidebar,
drawerPosition
});
}
);
export default Routes;

View File

@ -3,6 +3,7 @@ import 'regenerator-runtime/runtime';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import { composeWithDevTools } from 'remote-redux-devtools';
import reducers from '../reducers';
import sagas from '../sagas';
@ -13,9 +14,11 @@ let enhacers;
if (__DEV__) {
/* eslint-disable global-require */
const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
enhacers = composeWithDevTools(
applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware)
applyMiddleware(sagaMiddleware),
applyMiddleware(logger)
);
} else {
enhacers = composeWithDevTools(

View File

@ -2,29 +2,45 @@ import { CREATE_CHANNEL } from '../actions/actionsTypes';
const initialState = {
isFetching: false,
failure: false
failure: false,
users: []
};
export default function messages(state = initialState, action) {
switch (action.type) {
case CREATE_CHANNEL.REQUEST:
return { ...state,
return {
...state,
error: undefined,
failure: false,
isFetching: true
};
case CREATE_CHANNEL.SUCCESS:
return { ...state,
return {
...state,
isFetching: false,
failure: false,
result: action.data
};
case CREATE_CHANNEL.FAILURE:
return { ...state,
return {
...state,
isFetching: false,
failure: true,
error: action.err
};
case CREATE_CHANNEL.ADD_USER:
return {
...state,
users: state.users.concat(action.user)
};
case CREATE_CHANNEL.REMOVE_USER:
return {
...state,
users: state.users.filter(item => item.name !== action.user.name)
};
case CREATE_CHANNEL.RESET:
return initialState;
default:
return state;
}

View File

@ -1,31 +1,29 @@
import { delay } from 'redux-saga';
import { select, put, call, take, takeEvery } from 'redux-saga/effects';
import { select, put, call, take, takeLatest } from 'redux-saga/effects';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';
import { createChannelSuccess, createChannelFailure } from '../actions/createChannel';
import RocketChat from '../lib/rocketchat';
const create = function* create(data) {
return yield RocketChat.createChannel(data);
};
const get = function* get({ data }) {
try {
yield delay(1000);
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(LOGIN.SUCCESS);
}
const result = yield call(create, data);
yield put(createChannelSuccess(result));
select(({ navigator }) => navigator).dismissModal({
animationType: 'slide-down'
});
} catch (err) {
yield delay(2000);
yield put(createChannelFailure(err));
}
};
const getData = function* getData() {
yield takeEvery(CREATE_CHANNEL.REQUEST, get);
yield takeLatest(CREATE_CHANNEL.REQUEST, get);
};
export default getData;

View File

@ -6,20 +6,25 @@ import { createChannelRequest } from '../actions/createChannel';
import styles from './Styles';
import KeyboardView from '../presentation/KeyboardView';
@connect(state => ({
result: state.createChannel
}), dispatch => ({
@connect(
state => ({
result: state.createChannel,
users: state.createChannel.users
}),
dispatch => ({
createChannel: data => dispatch(createChannelRequest(data))
}))
})
)
export default class CreateChannelView extends React.Component {
static navigationOptions = () => ({
title: 'Create a New Channel'
});
static propTypes = {
createChannel: PropTypes.func.isRequired,
result: PropTypes.object.isRequired
}
result: PropTypes.object.isRequired,
users: PropTypes.array.isRequired,
navigation: PropTypes.object.isRequired
};
constructor(props) {
super(props);
@ -29,23 +34,42 @@ export default class CreateChannelView extends React.Component {
};
this.state = this.default;
}
componentDidUpdate() {
if (!this.adding) {
return;
}
if (this.props.result.result && !this.props.result.failure) {
this.props.navigation.navigate('Room', { room: this.props.result.result });
this.adding = false;
}
}
submit() {
this.adding = true;
if (!this.state.channelName.trim() || this.props.result.isFetching) {
return;
}
const { channelName, users = [], type = true } = this.state;
const { channelName, type = true } = this.state;
let { users } = this.props;
// transform users object into array of usernames
users = users.map(user => user.name);
// create channel
this.props.createChannel({ name: channelName, users, type });
}
renderChannelNameError() {
if (!this.props.result.failure || this.props.result.error.error !== 'error-duplicate-channel-name') {
if (
!this.props.result.failure ||
this.props.result.error.error !== 'error-duplicate-channel-name'
) {
return null;
}
return (
<Text style={[styles.label_white, styles.label_error]}>
{this.props.result.error.reason}
</Text>
<Text style={[styles.label_white, styles.label_error]}>{this.props.result.error.reason}</Text>
);
}
@ -91,18 +115,22 @@ export default class CreateChannelView extends React.Component {
flexGrow: 1,
paddingHorizontal: 0,
marginBottom: 20
}]}
}
]}
>
{this.state.type ?
'Everyone can access this channel' :
'Just invited people can access this channel'}
{this.state.type ? (
'Everyone can access this channel'
) : (
'Just invited people can access this channel'
)}
</Text>
<TouchableOpacity
onPress={() => this.submit()}
style={[
styles.buttonContainer_white,
(this.state.channelName.length === 0 || this.props.result.isFetching) ?
styles.disabledButton : styles.enabledButton
this.state.channelName.length === 0 || this.props.result.isFetching
? styles.disabledButton
: styles.enabledButton
]}
>
<Text style={styles.button_white}>

View File

@ -43,16 +43,18 @@ const styles = StyleSheet.create({
}
});
@connect(state => ({
@connect(
state => ({
server: state.server.server,
Site_Url: state.settings.Site_Url,
Message_TimeFormat: state.settings.Message_TimeFormat,
loading: state.messages.isFetching
}), dispatch => ({
}),
dispatch => ({
actions: bindActionCreators(actions, dispatch),
getMessages: rid => dispatch(messagesRequest({ rid }))
}))
})
)
export default class RoomView extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
@ -64,15 +66,21 @@ export default class RoomView extends React.Component {
Site_Url: PropTypes.string,
Message_TimeFormat: PropTypes.string,
loading: PropTypes.bool
}
};
constructor(props) {
super(props);
this.sid = props.navigation.state.params.room.sid;
this.rid = props.rid || realm.objectForPrimaryKey('subscriptions', this.sid).rid;
this.rid =
props.rid ||
props.navigation.state.params.room.rid ||
realm.objectForPrimaryKey('subscriptions', this.sid).rid;
this.data = realm.objects('messages').filtered('_server.id = $0 AND rid = $1', this.props.server, 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 = {
slow: false,
dataSource: [],
@ -83,7 +91,10 @@ export default class RoomView extends React.Component {
componentWillMount() {
this.props.navigation.setParams({
title: this.props.name || realm.objectForPrimaryKey('subscriptions', this.sid).name
title:
this.props.name ||
this.props.navigation.state.params.room.name ||
realm.objectForPrimaryKey('subscriptions', this.sid).name
});
this.timer = setTimeout(() => this.setState({ slow: true }), 5000);
this.props.getMessages(this.rid);
@ -103,7 +114,12 @@ export default class RoomView extends React.Component {
onEndReached = () => {
const rowCount = this.state.dataSource.getRowCount();
if (rowCount && this.state.loaded && this.state.loadingMore !== true && this.state.end !== true) {
if (
rowCount &&
this.state.loaded &&
this.state.loadingMore !== true &&
this.state.end !== true
) {
this.setState({
// ...this.state,
loadingMore: true
@ -118,7 +134,7 @@ export default class RoomView extends React.Component {
});
});
}
}
};
updateState = () => {
this.setState({
@ -135,13 +151,12 @@ export default class RoomView extends React.Component {
});
};
renderBanner = () => (this.state.slow && this.props.loading ?
(
renderBanner = () =>
(this.state.slow && this.props.loading ? (
<View style={styles.bannerContainer}>
<Text style={styles.bannerText}>Loading new messages...</Text>
</View>
) : null)
) : null);
renderItem = ({ item }) => (
<Message
@ -152,9 +167,7 @@ export default class RoomView extends React.Component {
/>
);
renderSeparator = () => (
<View style={styles.separator} />
);
renderSeparator = () => <View style={styles.separator} />;
renderFooter = () => {
if (!this.state.joined) {
@ -165,14 +178,8 @@ export default class RoomView extends React.Component {
</View>
);
}
return (
<MessageBox
ref={box => this.box = box}
onSubmit={this.sendMessage}
rid={this.rid}
/>
);
}
return <MessageBox ref={box => (this.box = box)} onSubmit={this.sendMessage} rid={this.rid} />;
};
renderHeader = () => {
if (this.state.loadingMore) {
@ -182,7 +189,7 @@ export default class RoomView extends React.Component {
if (this.state.end) {
return <Text style={styles.header}>Start of conversation</Text>;
}
}
};
render() {
return (

View File

@ -207,7 +207,7 @@ export default class RoomsListView extends React.Component {
}
_createChannel() {
this.props.navigation.navigate('CreateChannel');
this.props.navigation.navigate('SelectUsers');
}
renderSearchBar = () => (

View File

@ -0,0 +1,271 @@
import ActionButton from 'react-native-action-button';
import { ListView } from 'realm/react-native';
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/Ionicons';
import { View, StyleSheet, TextInput, Text, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import * as actions from '../actions';
import * as server from '../actions/connect';
import * as createChannelActions from '../actions/createChannel';
import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat';
import RoomItem from '../presentation/RoomItem';
import Banner from '../containers/Banner';
import Avatar from '../containers/Avatar';
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'stretch',
justifyContent: 'center'
},
list: {
width: '100%',
backgroundColor: '#FFFFFF'
},
actionButtonIcon: {
fontSize: 20,
height: 22,
color: 'white'
},
searchBoxView: {
backgroundColor: '#eee'
},
searchBox: {
backgroundColor: '#fff',
margin: 5,
borderRadius: 5,
padding: 5,
paddingLeft: 10,
color: '#aaa'
},
selectItemView: {
width: 80,
height: 80,
padding: 8,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}
});
const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
@connect(
state => ({
server: state.server.server,
login: state.login,
Site_Url: state.settings.Site_Url,
users: state.createChannel.users
}),
dispatch => ({
login: () => dispatch(actions.login()),
connect: () => dispatch(server.connectRequest()),
addUser: user => dispatch(createChannelActions.addUser(user)),
removeUser: user => dispatch(createChannelActions.removeUser(user)),
resetCreateChannel: () => dispatch(createChannelActions.reset())
})
)
export default class RoomsListView extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
Site_Url: PropTypes.string,
server: PropTypes.string,
addUser: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
resetCreateChannel: PropTypes.func.isRequired,
users: PropTypes.array
};
constructor(props) {
super(props);
this.data = realm
.objects('subscriptions')
.filtered('_server.id = $0 AND t = $1', this.props.server, 'd');
this.state = {
dataSource: ds.cloneWithRows(this.data),
searching: false,
searchDataSource: [],
searchText: '',
login: false
};
this.data.addListener(this.updateState);
}
componentWillUnmount() {
this.data.removeListener(this.updateState);
this.props.resetCreateChannel();
}
onSearchChangeText = (text) => {
const searchText = text.trim();
this.setState({
searchText: text,
searching: searchText !== ''
});
if (searchText === '') {
return this.setState({
dataSource: ds.cloneWithRows(this.data)
});
}
const data = this.data.filtered('name CONTAINS[c] $0', searchText).slice();
const usernames = [];
const dataSource = data.map((sub) => {
if (sub.t === 'd') {
usernames.push(sub.name);
}
return sub;
});
if (dataSource.length < 7) {
if (this.oldPromise) {
this.oldPromise();
}
Promise.race([
RocketChat.spotlight(searchText, usernames),
new Promise((resolve, reject) => (this.oldPromise = reject))
])
.then(
(results) => {
results.users.forEach((user) => {
dataSource.push({
...user,
name: user.username,
t: 'd',
search: true
});
});
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
},
() => console.log('spotlight stopped')
)
.then(() => delete this.oldPromise);
}
this.setState({
dataSource: ds.cloneWithRows(dataSource)
});
};
updateState = () => {
this.setState({
dataSource: ds.cloneWithRows(this.data)
});
};
toggleUser = (user) => {
const index = this.props.users.findIndex(el => el.name === user.name);
if (index === -1) {
this.props.addUser(user);
} else {
this.props.removeUser(user);
}
};
_onPressItem = (id, item = {}) => {
if (item.search) {
this.toggleUser({ _id: item._id, name: item.username });
} else {
this.toggleUser({ _id: item._id, name: item.name });
}
};
_onPressSelectedItem = item => this.toggleUser(item);
_createChannel = () => {
this.props.navigation.navigate('CreateChannel');
};
renderHeader = () => (
<View style={styles.container}>
{this.renderSearchBar()}
{this.renderSelected()}
</View>
);
renderSearchBar = () => (
<View style={styles.searchBoxView}>
<TextInput
underlineColorAndroid='transparent'
style={styles.searchBox}
value={this.state.searchText}
onChangeText={this.onSearchChangeText}
returnKeyType='search'
placeholder='Search'
clearButtonMode='while-editing'
blurOnSubmit
/>
</View>
);
renderSelected = () => {
if (this.props.users.length === 0) {
return null;
}
const usersDataSource = ds.cloneWithRows(this.props.users);
return (
<ListView
dataSource={usersDataSource}
style={styles.list}
renderRow={this.renderSelectedItem}
enableEmptySections
keyboardShouldPersistTaps='always'
horizontal
/>
);
};
renderSelectedItem = item => (
<TouchableOpacity
key={item._id}
style={styles.selectItemView}
onPress={() => this._onPressSelectedItem(item)}
>
<Avatar text={item.name} baseUrl={this.props.Site_Url} size={40} borderRadius={20} />
<Text ellipsizeMode='tail' numberOfLines={1} style={{ fontSize: 10 }}>
{item.name}
</Text>
</TouchableOpacity>
);
renderItem = item => (
<RoomItem
key={item._id}
name={item.name}
type={item.t}
baseUrl={this.props.Site_Url}
onPress={() => this._onPressItem(item._id, item)}
/>
);
renderList = () => (
<ListView
dataSource={this.state.dataSource}
style={styles.list}
renderRow={this.renderItem}
renderHeader={this.renderHeader}
contentOffset={{ x: 0, y: this.props.users.length > 0 ? 40 : 20 }}
enableEmptySections
keyboardShouldPersistTaps='always'
/>
);
renderCreateButton = () => {
if (this.props.users.length === 0) {
return null;
}
return (
<ActionButton
buttonColor='rgba(67, 165, 71, 1)'
onPress={() => this._createChannel()}
icon={<Icon name='md-arrow-forward' style={styles.actionButtonIcon} />}
/>
);
};
render = () => (
<View style={styles.container}>
<Banner />
{this.renderList()}
{this.renderCreateButton()}
</View>
);
}

View File

@ -800,7 +800,6 @@
TestTargetID = 13B07F861A680F5B00A75B9A;
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = S6UPZG7ZR3;
ProvisioningStyle = Automatic;
};
2D02E47A1E0B4A5D006451C7 = {
@ -1379,7 +1378,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = S6UPZG7ZR3;
DEVELOPMENT_TEAM = "";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/realm/src/**",
@ -1412,7 +1411,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = S6UPZG7ZR3;
DEVELOPMENT_TEAM = "";
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(SRCROOT)/../node_modules/realm/src/**",

5484
package-lock.json generated

File diff suppressed because it is too large Load Diff