Added initial version of MainView/MainContainer

This commit is contained in:
Harrison Healey 2016-11-09 12:52:17 -05:00
parent 71c0707a90
commit 86b33e0164
30 changed files with 750 additions and 382 deletions

View file

@ -3,19 +3,23 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"babel-preset-es2015": "6.18.0",
"intl": "1.2.5",
"isomorphic-fetch": "2.2.1",
"lodash": "4.16.4",
"react": "15.3.2",
"react-addons-pure-render-mixin": "15.3.2",
"react-intl": "2.1.5",
"react-native": "0.34.1",
"react-native-button": "1.7.1",
"react-native-drawer": "2.3.0",
"react-native-keyboard-spacer": "0.3.0",
"react-native-router-flux": "3.36.0",
"react-native-vector-icons": "2.1.0",
"react-redux": "4.4.5",
"redux": "3.6.0",
"redux-thunk": "2.1.0",
"react-native-vector-icons": "2.1.0"
"redux-batched-actions": "0.1.3",
"redux-thunk": "2.1.0"
},
"devDependencies": {
"babel-eslint": "7.0.0",

View file

@ -1,22 +1,30 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindClientFunc} from './helpers.js';
import Client from 'client/client_instance';
import {ChannelsTypes} from 'constants';
import {ChannelTypes} from 'constants';
import {batchActions} from 'redux-batched-actions';
import Client from 'client/client_instance.js';
export function selectChannel(channel) {
return {
type: ChannelsTypes.SELECT_CHANNEL,
channelId: channel.id
export function fetchMyChannelsAndMembers(teamId) {
return async (dispatch, getState) => {
try {
const channels = Client.getChannels(teamId);
const channelMembers = Client.getMyChannelMembers(teamId);
dispatch(batchActions([
{
type: ChannelTypes.CHANNELS_RECEIVED,
teamId,
channels: await channels
},
{
type: ChannelTypes.CHANNEL_MEMBERS_RECEIVED,
teamId,
channelMembers: await channelMembers
}
]), getState);
} catch (err) {
console.error(err); // eslint-disable-line no-console
}
};
}
export function fetchChannels() {
return bindClientFunc(
Client.fetchChannels,
ChannelsTypes.FETCH_CHANNELS_REQUEST,
ChannelsTypes.FETCH_CHANNELS_SUCCESS,
ChannelsTypes.FETCH_CHANNELS_FAILURE
);
}

View file

@ -15,7 +15,7 @@ export function selectTeam(team) {
export function fetchTeams() {
return bindClientFunc(
Client.fetchTeams,
Client.getAllTeams,
TeamsTypes.FETCH_TEAMS_REQUEST,
TeamsTypes.FETCH_TEAMS_SUCCESS,
TeamsTypes.FETCH_TEAMS_FAILURE

View file

@ -11,7 +11,6 @@ const CONTENT_TYPE_JSON = 'application/json';
export default class Client {
constructor() {
this.teamId = '';
this.logToConsole = false;
this.token = '';
this.url = '';
@ -27,18 +26,6 @@ export default class Client {
this.url = url;
}
setTeamId(id) {
this.teamId = id;
}
getTeamId() {
if (!this.teamId) {
console.error('You are trying to use a route that requires a team_id, but you have not called setTeamId() in client.jsx'); // eslint-disable-line no-console
}
return this.teamId;
}
getBaseRoute() {
return `${this.url}${this.urlVersion}`;
}
@ -59,16 +46,16 @@ export default class Client {
return `${this.url}${this.urlVersion}/teams`;
}
getTeamNeededRoute() {
return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}`;
getTeamNeededRoute(teamId) {
return `${this.url}${this.urlVersion}/teams/${teamId}`;
}
getChannelsRoute() {
return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels`;
getChannelsRoute(teamId) {
return `${this.url}${this.urlVersion}/teams/${teamId}/channels`;
}
getChannelNameRoute(channelName) {
return `${this.url}${this.urlVersion}/teams/${this.getTeamId()}/channels/name/${channelName}`;
getChannelNameRoute(teamId, channelName) {
return `${this.url}${this.urlVersion}/teams/${teamId}/channels/name/${channelName}`;
}
getChannelNeededRoute(channelId) {
@ -209,7 +196,14 @@ export default class Client {
);
}
fetchTeams = async () => {
getAllTeams = async() => {
return this.doFetch(
`${this.getTeamsRoute()}/all`,
{method: 'get'}
);
}
getAllTeamListings = async () => {
return this.doFetch(
`${this.getTeamsRoute()}/all_team_listings`,
{method: 'get'}
@ -225,9 +219,16 @@ export default class Client {
);
}
fetchChannels = async () => {
getChannels = async (teamId) => {
return this.doFetch(
`${this.getChannelsRoute()}/`,
`${this.getChannelsRoute(teamId)}/`,
{method: 'get'}
);
}
getMyChannelMembers = async (teamId) => {
return this.doFetch(
`${this.getChannelsRoute(teamId)}/members`,
{method: 'get'}
);
}

View file

@ -0,0 +1,59 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
// import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Text, View} from 'react-native';
class ChannelSidebar extends React.Component {
static propTypes = {
currentTeam: React.PropTypes.object.isRequired,
channels: React.PropTypes.array.isRequired,
onSelectChannel: React.PropTypes.func
};
onSelectChannel = (channel) => {
console.log('clicked channel ' + channel.name); // eslint-disable-line no-console
// this.props.onSelectChannel(channel);
}
render() {
const channels = this.props.channels.map((channel) => {
return (
<Text
key={channel.id}
style={{height: 30, width: 100}}
onPress={() => this.onSelectChannel(channel)}
>
{channel.name}
</Text>
);
});
return (
<View
style={{
backgroundColor: 'gray',
flex: 1,
paddingTop: 20
}}
>
<Text style={{height: 60, width: 100}}>{this.props.currentTeam.name}</Text>
{channels}
</View>
);
}
}
function mapStateToProps(state, ownProps) {
return {
...ownProps,
channels: Object.keys(state.entities.channel.channelIdsByTeamId[ownProps.currentTeam.id] || {}).map((channelId) => state.entities.channel.channels[channelId])
};
}
export default connect(mapStateToProps)(ChannelSidebar);

View file

@ -1,66 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {View, Text} from 'react-native';
import {Actions as Routes} from 'react-native-router-flux';
import Button from 'react-native-button';
import _ from 'lodash';
import ErrorText from 'components/error_text';
import FormattedText from 'components/formatted_text';
import {GlobalStyles} from 'styles';
const propTypes = {
teams: PropTypes.object.isRequired,
channels: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
class ChannelsListView extends Component {
static propTypes = propTypes;
componentWillMount() {
this.props.actions.fetchChannels();
}
componentWillReceiveProps(nextProps) {
const {currentChannelId} = nextProps.channels;
if (currentChannelId &&
currentChannelId !== this.props.channels.currentChannelId) {
Routes.goToPostsList();
}
}
render() {
const {data: teams, currentTeamId} = this.props.teams;
const currentTeam = teams[currentTeamId];
const channels = _.values(this.props.channels.data);
return (
<View style={GlobalStyles.container}>
<Text style={GlobalStyles.header}>
{currentTeam.display_name}
</Text>
<FormattedText
style={GlobalStyles.subheader}
id='mobile.components.channels_list_view.yourChannels'
defaultMessage='Your channels:'
/>
{_.map(channels, (channel) => (
<Button
key={channel.id}
onPress={() => this.props.actions.selectChannel(channel)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
>
{channel.display_name}
</Button>
))}
<ErrorText error={this.props.channels.error}/>
</View>
);
}
}
export default ChannelsListView;

24
src/components/drawer.js Normal file
View file

@ -0,0 +1,24 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import BaseDrawer from 'react-native-drawer';
// Extends react-native-drawer to allow better control over the open/closed state from the parent
export default class Drawer extends BaseDrawer {
static propTypes = {
...BaseDrawer.propTypes,
onRequestClose: React.PropTypes.func.isRequired
};
close = (type, callback) => {
this.props.onRequestClose();
if (typeof type === 'function') {
// This function can be called with only a callback as its only argument
type();
} else if (callback) {
callback();
}
}
}

View file

@ -14,28 +14,30 @@ import logo from 'images/logo.png';
import {injectIntl, intlShape} from 'react-intl';
const propTypes = {
intl: intlShape.isRequired,
clientConfig: PropTypes.object.isRequired,
login: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
class LoginView extends Component {
static propTypes = propTypes;
static propTypes = {
intl: intlShape.isRequired,
clientConfig: PropTypes.object.isRequired,
login: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
state = {
loginId: '',
password: ''
};
componentWillMount() {
this.props.actions.getClientConfig();
}
componentWillReceiveProps(nextProps) {
if (this.props.login.status === 'fetching' &&
nextProps.login.status === 'fetched') {
if (this.props.login.status === 'fetching' && nextProps.login.status === 'fetched') {
Routes.goToSelectTeam();
}
}
signIn() {
signIn = () => {
if (this.props.login.status !== 'fetching') {
const {loginId, password} = this.state;
this.props.actions.login(loginId, password);
@ -44,7 +46,7 @@ class LoginView extends Component {
createLoginPlaceholder() {
const {formatMessage} = this.props.intl;
const clientConfig = this.props.clientConfig.data;
const clientConfig = this.props.clientConfig;
const loginPlaceholders = [];
if (clientConfig.EnableSignInWithEmail === 'true') {
@ -75,7 +77,7 @@ class LoginView extends Component {
}
render() {
if (!this.props.clientConfig || !this.props.clientConfig.data) {
if (this.props.clientConfig.loading || this.props.clientConfig.error) {
return <Loading/>;
}
@ -86,7 +88,7 @@ class LoginView extends Component {
source={logo}
/>
<Text style={GlobalStyles.header}>
{this.props.clientConfig.data.SiteName}
{this.props.clientConfig.SiteName}
</Text>
<FormattedText
style={GlobalStyles.subheader}
@ -94,6 +96,7 @@ class LoginView extends Component {
defaultMessage='All team communication in one place, searchable and accessible anywhere'
/>
<TextInput
ref='loginId'
value={this.state.loginId}
onChangeText={(loginId) => this.setState({loginId})}
style={GlobalStyles.inputBox}
@ -111,8 +114,10 @@ class LoginView extends Component {
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
returnKeyType='go'
onSubmitEditing={this.signIn}
/>
<Button onPress={() => this.signIn()}>
<Button onPress={this.signIn}>
<FormattedText
id='login.signIn'
defaultMessage='Sign in'

View file

@ -0,0 +1,99 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import Drawer from 'react-native-drawer';
import ChannelSidebar from './channel_sidebar.js';
import Loading from './loading.js';
import RightSidebarMenu from './right_sidebar_menu.js';
import {StatusBar, Text, TouchableHighlight, View} from 'react-native';
export default class MainView extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired,
currentTeam: React.PropTypes.object,
currentChannel: React.PropTypes.object
}
constructor(props) {
super(props);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
this.state = {
leftSidebarOpen: false,
rightSidebarOpen: false
};
}
componentWillMount() {
this.props.actions.fetchMyChannelsAndMembers(this.props.currentTeam.Id);
}
openLeftSidebar = () => {
this.setState({leftSidebarOpen: true});
}
closeLeftSidebar = () => {
this.setState({leftSidebarOpen: false});
}
openRightSidebar = () => {
this.setState({rightSidebarOpen: true});
}
closeRightSidebar = () => {
this.setState({rightSidebarOpen: false});
}
render() {
if (!this.props.currentChannel) {
return <Loading/>;
}
return (
<View style={{flex: 1}}>
<StatusBar barStyle='default'/>
<Drawer
open={this.state.leftSidebarOpen}
type='displace'
content={<ChannelSidebar currentTeam={this.props.currentTeam}/>}
side='left'
tapToClose={true}
onCloseStart={this.closeLeftSidebar}
openDrawerOffset={0.2}
>
<Drawer
open={this.state.rightSidebarOpen}
type='displace'
content={<RightSidebarMenu/>}
side='right'
tapToClose={true}
onCloseStart={this.closeRightSidebar}
openDrawerOffset={0.2}
>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingTop: 20}}>
<TouchableHighlight
onPress={this.openLeftSidebar}
style={{height: 50, width: 50}}
>
<Text>{'<'}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={this.openRightSidebar}
style={{height: 50, width: 50}}
>
<Text>{'>'}</Text>
</TouchableHighlight>
</View>
<Text>{this.props.currentTeam.id + ' - ' + this.props.currentTeam.name}</Text>
<Text>{this.props.currentChannel.id + ' - ' + this.props.currentChannel.name}</Text>
</Drawer>
</Drawer>
</View>
);
}
}

View file

@ -1,50 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import _ from 'lodash';
import ErrorText from 'components/error_text';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
alignItems: 'center',
paddingTop: 200,
backgroundColor: 'white'
}
});
const propTypes = {
posts: PropTypes.object.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired
};
export default class PostsListView extends Component {
static propTypes = propTypes;
componentWillMount() {
this.props.actions.fetchPosts(
this.props.currentTeamId,
this.props.currentChannelId
);
}
render() {
const posts = _.values(this.props.posts.data);
return (
<View style={styles.container}>
{posts.map((post) => (
<Text key={post.id}>
{`${post.user_id} - ${post.message}`}
</Text>
))}
<ErrorText error={this.props.posts.error}/>
</View>
);
}
}

View file

@ -0,0 +1,208 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import FormattedText from './formatted_text.js';
import Icon from 'react-native-vector-icons/FontAwesome';
import {
ScrollView,
StyleSheet,
Text,
TouchableHighlight,
View
} from 'react-native';
const Styles = StyleSheet.create({
container: {
backgroundColor: '#2071a7',
flex: 1,
paddingTop: 20
},
item: {
alignItems: 'center',
height: 40,
paddingLeft: 10,
paddingRight: 10,
flex: 1,
flexDirection: 'row'
},
itemText: {
color: 'white'
},
icon: {
color: 'white',
width: 25
},
mentionIcon: {
fontSize: 17,
fontWeight: 'bold'
},
divider: {
borderColor: 'rgba(255, 255, 255, 0.6)',
borderTopWidth: StyleSheet.hairlineWidth
}
});
export default class RightSidebarMenu extends React.Component {
render() {
return (
<ScrollView style={Styles.container}>
<Item>
<Text style={[Styles.icon, Styles.mentionIcon]}>{'@'}</Text>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.recentMentions'
defaultMessage='Recent Mentions'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='flag'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.flagged'
defaultMessage='Flagged Posts'
/>
</Item>
<Divider/>
<Item>
<Icon
style={Styles.icon}
name='cog'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.accountSettings'
defaultMessage='Account Settings'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='user-plus'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.inviteNew'
defaultMessage='Invite New Member'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='link'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.teamLink'
defaultMessage='Get Team Invite Link'
/>
</Item>
<Divider/>
<Item>
<Icon
style={Styles.icon}
name='globe'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.teamSettings'
defaultMessage='Team Settings'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='users'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.manageMembers'
defaultMessage='Manage Members'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='exchange'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.switch_team'
defaultMessage='Team Selection'
/>
</Item>
<Divider/>
<Item>
<Icon
style={Styles.icon}
name='question'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.help'
defaultMessage='Help'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='phone'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.report'
defaultMessage='Report a Problem'
/>
</Item>
<Item>
<Icon
style={Styles.icon}
name='info'
/>
<FormattedText
style={Styles.itemText}
id='navbar_dropdown.about'
defaultMessage='About Mattermost'
/>
</Item>
<Divider/>
<Item>
<Icon
style={Styles.icon}
name='sign-out'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.logout'
defaultMessage='Logout'
/>
</Item>
</ScrollView>
);
}
}
function Item({onPress, children}) {
return (
<TouchableHighlight
underlayColor='rgba(255, 255, 255, 0.3)'
onPress={onPress}
>
<View style={Styles.item}>
{children}
</View>
</TouchableHighlight>
);
}
Item.propTypes = {
onPress: React.PropTypes.func.isRequired,
children: React.PropTypes.node
};
function Divider() {
return <View style={Styles.divider}/>;
}

View file

@ -35,16 +35,12 @@ class SelectServerView extends Component {
onClick = () => {
Client.setUrl(this.state.serverUrl);
this.props.actions.getClientConfig();
Routes.goToLogin();
// this.props.actions.getPing().then(() => {
// AsyncStorage.setItem('serverUrl', this.state.serverUrl, () => {
// if (!this.props.ping.error) {
// this.props.onProceed();
// }
// });
// });
this.props.actions.getPing().then(() => {
if (!this.props.ping.loading && !this.props.ping.error) {
Routes.goToLogin();
}
});
}
render() {

View file

@ -6,37 +6,54 @@ import React, {Component, PropTypes} from 'react';
import {View, Image, Text} from 'react-native';
import {Actions as Routes} from 'react-native-router-flux';
import Button from 'react-native-button';
import Loading from 'components/loading.js';
import Icon from 'react-native-vector-icons/MaterialIcons';
import _ from 'lodash';
import ErrorText from 'components/error_text';
import {GlobalStyles} from 'styles';
import logo from 'images/logo.png';
import FormattedText from 'components/formatted_text';
const propTypes = {
clientConfig: PropTypes.object.isRequired,
teams: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
export default class SelectTeamView extends Component {
static propTypes = propTypes;
static propTypes = {
clientConfig: PropTypes.object.isRequired,
teams: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
componentWillMount() {
this.props.actions.fetchTeams();
}
componentWillReceiveProps(nextProps) {
const {currentTeamId} = nextProps.teams;
if (currentTeamId &&
currentTeamId !== this.props.teams.currentTeamId) {
Routes.goToChannelsList();
}
onSelectTeam(team) {
Routes.goToMain({currentTeamId: team.id});
}
render() {
const teams = _.values(this.props.teams.data);
if (this.props.teams.status !== 'fetched') {
return <Loading/>;
}
const teams = [];
for (const id of Object.keys(this.props.teams.data)) {
const team = this.props.teams.data[id];
teams.push(
<Button
key={id}
onPress={() => this.onSelectTeam(team)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
>
{team.display_name}
<Icon
name='keyboard-arrow-right'
size={24}
color='#777'
/>
</Button>
);
}
return (
<View style={GlobalStyles.container}>
@ -45,7 +62,7 @@ export default class SelectTeamView extends Component {
source={logo}
/>
<Text style={GlobalStyles.header}>
{this.props.clientConfig.data.SiteName}
{this.props.clientConfig.SiteName}
</Text>
<FormattedText
style={GlobalStyles.subheader}
@ -57,21 +74,7 @@ export default class SelectTeamView extends Component {
id='signup_team.choose'
defaultMessage='Your teams:'
/>
{_.map(teams, (team) => (
<Button
key={team.id}
onPress={() => this.props.actions.selectTeam(team)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
>
{team.display_name}
<Icon
name='keyboard-arrow-right'
size={24}
color='#777'
/>
</Button>
))}
{teams}
<ErrorText error={this.props.teams.error}/>
</View>
);

8
src/constants/channel.js Normal file
View file

@ -0,0 +1,8 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export const CHANNEL_RECEIVED = 'mattermost/channels/channel_received';
export const CHANNELS_RECEIVED = 'mattermost/channels/channels_received';
export const CHANNEL_MEMBER_RECEIVED = 'mattermost/channels/channel_member_received';
export const CHANNEL_MEMBERS_RECEVED = 'mattermost/channels/channel_members_received';

View file

@ -1,8 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export const SELECT_CHANNEL = 'SELECT_CHANNEL';
export const FETCH_CHANNELS_REQUEST = 'FETCH_CHANNELS_REQUEST';
export const FETCH_CHANNELS_SUCCESS = 'FETCH_CHANNELS_SUCCESS';
export const FETCH_CHANNELS_FAILURE = 'FETCH_CHANNELS_FAILURE';

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as ChannelsTypes from './channels';
import * as ChannelTypes from './channel';
import * as DeviceTypes from './device';
import * as GeneralTypes from './general';
import * as LoginTypes from './login';
@ -15,6 +15,6 @@ export {
LoginTypes,
LogoutTypes,
TeamsTypes,
ChannelsTypes,
ChannelTypes,
PostsTypes
};

View file

@ -1,22 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as channelActions from 'actions/channels';
import ChannelsListView from 'components/channels_list_view';
function mapStateToProps(state) {
return {
teams: state.entities.teams,
channels: state.entities.channels
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(channelActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelsListView);

View file

@ -4,6 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as loginActions from 'actions/login';
import {getClientConfig} from 'actions/general';
import LoginView from 'components/login_view';
function mapStateToProps(state) {
@ -15,7 +16,10 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(loginActions, dispatch)
actions: bindActionCreators({
...loginActions,
getClientConfig
}, dispatch)
};
}

View file

@ -0,0 +1,42 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {fetchMyChannelsAndMembers} from 'actions/channels.js';
import MainView from 'components/main_view.js';
function mapStateToProps(state, ownProps) {
let currentChannel;
if (ownProps.currentChannelId) {
currentChannel = state.entities.channel.channels[ownProps.currentChannelId];
} else {
// TODO figure out the town square id before this
const channelIds = state.entities.channel.channelIdsByTeamId[ownProps.currentTeamId] || {};
for (const channelId of Object.keys(channelIds)) {
const channel = state.entities.channel.channels[channelId];
if (channel.name === 'town-square') {
currentChannel = channel;
break;
}
}
}
return {
...ownProps,
currentTeam: state.entities.teams.data[ownProps.currentTeamId] || {},
currentChannel
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
fetchMyChannelsAndMembers
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MainView);

View file

@ -1,23 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as postActions from 'actions/posts';
import PostsListView from 'components/posts_list_view';
function mapStateToProps(state) {
return {
posts: state.entities.posts,
currentTeamId: state.entities.teams.currentTeamId,
currentChannelId: state.entities.channels.currentChannelId
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(postActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PostsListView);

View file

@ -3,7 +3,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getPing, getClientConfig} from 'actions/general';
import {getPing} from 'actions/general';
import SelectServerView from 'components/select_server_view';
function mapStateToProps(state) {
@ -15,7 +15,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({getPing, getClientConfig}, dispatch)
actions: bindActionCreators({getPing}, dispatch)
};
}

99
src/reducers/channel.js Normal file
View file

@ -0,0 +1,99 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ChannelTypes} from 'constants';
import {combineReducers} from 'redux';
export function channels(state = {}, action) {
switch (action.type) {
case ChannelTypes.CHANNEL_RECEIVED:
return {
...state,
[action.channel.id]: action.channel
};
case ChannelTypes.CHANNELS_RECEIVED: {
const nextState = {...state};
for (const channel of action.channels) {
nextState[channel.id] = channel;
}
return nextState;
}
default:
return state;
}
}
export function channelIdsByTeamId(state = {}, action) {
switch (action.type) {
case ChannelTypes.CHANNEL_RECEIVED: {
const channel = action.channel;
return {
...state,
[channel.team_id]: {
...state[channel.team_id],
[channel.id]: channel.id
}
};
}
case ChannelTypes.CHANNELS_RECEIVED: {
const nextState = {...state};
for (const channel of action.channels) {
nextState[channel.team_id] = {
...nextState[channel.team_id],
[channel.id]: channel.id
};
}
return nextState;
}
default:
return state;
}
}
export function channelMembers(state = {}, action) {
switch (action.type) {
case ChannelTypes.CHANNEL_MEMBER_RECEIVED: {
const channelMember = action.channelMember;
return {
...state,
[`${channelMember.channel_id}-${channelMember.user_id}`]: channelMember
};
}
case ChannelTypes.CHANNEL_MEMBERS_RECEIVED: {
const nextState = {...state};
for (const channelMember of action.channelMembers) {
nextState[`${channelMember.channel_id}-${channelMember.user_id}`] = channelMember;
}
return nextState;
}
default:
return state;
}
}
export default combineReducers({
// A map of channel IDs to channels
channels,
// A map of team IDs to pseudo-sets of channelIds
channelIdsByTeamId,
// A map of "channelId-userId" to channel members
channelMembers
});

View file

@ -1,48 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import _ from 'lodash';
import {ChannelsTypes, LogoutTypes} from 'constants';
export const initState = {
status: 'not fetched',
error: null,
data: {},
currentChannelId: null
};
// TODO: this can be extracted into a util
function zipById(channels) {
return _.zipObject(_.map(channels, 'id'), channels);
}
export default function reduceChannels(state = initState, action) {
switch (action.type) {
case ChannelsTypes.SELECT_CHANNEL:
return {...state,
currentChannelId: action.channelId
};
case ChannelsTypes.FETCH_CHANNELS_REQUEST:
return {...state,
status: 'fetching',
error: null
};
case ChannelsTypes.FETCH_CHANNELS_SUCCESS:
return {...state,
status: 'fetched',
data: zipById(action.data.channels)
};
case ChannelsTypes.FETCH_CHANNELS_FAILURE:
return {...state,
status: 'failed',
error: action.error
};
case LogoutTypes.LOGOUT_SUCCESS:
return initState;
default:
return state;
}
}

View file

@ -2,34 +2,62 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
import {initialState, handle} from './helpers.js';
import {initialState} from './helpers.js';
import {GeneralTypes} from 'constants';
export const initState = initialState();
// TODO: clientConfig should be cleared when the user logs out.
// We can't do so until it's extracted into its own store with more reducers
export function clientConfig(state = initState, action) {
return handle(
GeneralTypes.CLIENT_CONFIG_REQUEST,
GeneralTypes.CLIENT_CONFIG_SUCCESS,
GeneralTypes.CLIENT_CONFIG_FAILURE,
state,
action
);
function ping(state = {}, action) {
switch (action.type) {
case GeneralTypes.PING_REQUEST:
return {
...action.data,
loading: true
};
case GeneralTypes.PING_SUCCESS:
return {
...action.data,
loading: false,
error: null
};
case GeneralTypes.PING_FAILURE:
return {
...action.data,
loading: false,
error: action.error
};
default:
return state;
}
}
export function ping(state = initialState(), action) {
return handle(
GeneralTypes.PING_REQUEST,
GeneralTypes.PING_SUCCESS,
GeneralTypes.PING_FAILURE,
state,
action
);
function clientConfig(state = {}, action) {
switch (action.type) {
case GeneralTypes.CLIENT_CONFIG_REQUEST:
return {
...action.data,
loading: true
};
case GeneralTypes.CLIENT_CONFIG_SUCCESS:
return {
...action.data,
loading: false,
error: null
};
case GeneralTypes.CLIENT_CONFIG_FAILURE:
return {
...action.data,
loading: false,
error: action.error
};
default:
return state;
}
}
export default combineReducers({
clientConfig,
ping
ping,
clientConfig
});

View file

@ -3,7 +3,7 @@
import {combineReducers} from 'redux';
import channels from './channels';
import channel from './channel.js';
import device from './device.js';
import general from './general.js';
import login from './login';
@ -12,7 +12,7 @@ import teams from './teams';
import posts from './posts';
const entities = combineReducers({
channels,
channel,
general,
teams,
posts

View file

@ -12,16 +12,19 @@ export default function reduceLogin(state = initState, action) {
switch (action.type) {
case LoginTypes.LOGIN_REQUEST:
return {...state,
return {
...state,
status: 'fetching',
error: null
};
case LoginTypes.LOGIN_SUCCESS:
return {...state,
return {
...state,
status: 'fetched'
};
case LoginTypes.LOGIN_FAILURE:
return {...state,
return {
...state,
status: 'failed',
error: action.error
};

View file

@ -14,22 +14,26 @@ export default function reduceTeams(state = initState, action) {
switch (action.type) {
case TeamsTypes.SELECT_TEAM:
return {...state,
return {
...state,
currentTeamId: action.teamId
};
case TeamsTypes.FETCH_TEAMS_REQUEST:
return {...state,
return {
...state,
status: 'fetching',
error: null
};
case TeamsTypes.FETCH_TEAMS_SUCCESS:
return {...state,
return {
...state,
status: 'fetched',
data: action.data
};
case TeamsTypes.FETCH_TEAMS_FAILURE:
return {...state,
return {
...state,
status: 'failed',
error: action.error
};

View file

@ -1,15 +1,13 @@
/* eslint-disable
class-methods-use-this,
react/require-optimization */
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import {Scene, Router} from 'react-native-router-flux';
import ChannelsListContainer from 'containers/channels_list_container';
import LoginContainer from 'containers/login_container';
import SelectServerContainer from 'containers/select_server_container';
import SelectTeamContainer from 'containers/select_team_container';
import PostsListContainer from 'containers/posts_list_container';
import MainContainer from 'containers/main_container.js';
import Logout from 'components/logout';
import * as logout from 'actions/logout';
@ -26,11 +24,6 @@ class Routes extends Component {
return (
<Router>
<Scene key='root'>
<Scene
key='goToLogin'
component={LoginContainer}
title={formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'})}
/>
<Scene
key='goToSelectServer'
component={SelectServerContainer}
@ -38,12 +31,9 @@ class Routes extends Component {
initial={true}
/>
<Scene
key='goToChannelsList'
component={ChannelsListContainer}
title={formatMessage({id: 'mobile.routes.channels', defaultMessage: 'Channels'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
key='goToLogin'
component={LoginContainer}
title={formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'})}
/>
<Scene
key='goToSelectTeam'
@ -54,12 +44,9 @@ class Routes extends Component {
}
/>
<Scene
key='goToPostsList'
component={PostsListContainer}
title={formatMessage({id: 'mobile.routes.postsList', defaultMessage: 'Posts List'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
key='goToMain'
component={MainContainer}
hideNavBar={true}
/>
</Scene>
</Router>

View file

@ -1,21 +1,23 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {applyMiddleware, createStore} from 'redux';
import {composeWithDevTools} from 'remote-redux-devtools';
import {applyMiddleware, compose, createStore} from 'redux';
import devTools from 'remote-redux-devtools';
import {enableBatching} from 'redux-batched-actions';
import rootReducer from 'reducers/index.js';
import thunk from 'redux-thunk';
export default function configureStore(preloadedState) {
const store = createStore(
rootReducer,
enableBatching(rootReducer),
preloadedState,
composeWithDevTools({
name: 'Mattermost',
hostname: 'localhost',
port: 5678
})(
applyMiddleware(thunk)
compose(
applyMiddleware(thunk),
devTools({
name: 'Mattermost',
hostname: 'localhost',
port: 5678
})
)
);
@ -28,4 +30,4 @@ export default function configureStore(preloadedState) {
}
return store;
}
}

View file

@ -2,13 +2,14 @@
// See License.txt for license information.
import {applyMiddleware, createStore} from 'redux';
import {enableBatching} from 'redux-batched-actions';
import rootReducer from 'reducers/index.js';
import thunk from 'redux-thunk';
export default function configureStore(preloadedState) {
return createStore(
rootReducer,
enableBatching(rootReducer),
preloadedState,
applyMiddleware(thunk)
);
}
}