diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js
new file mode 100644
index 000000000..dabd43188
--- /dev/null
+++ b/app/actions/views/channel.js
@@ -0,0 +1,49 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {fetchMyChannelsAndMembers, selectChannel} from 'service/actions/channels';
+import {getPosts} from 'service/actions/posts';
+import {Constants} from 'service/constants';
+
+export function loadChannelsIfNecessary(teamId) {
+ return async (dispatch, getState) => {
+ const channels = getState().entities.channels.channels;
+
+ let hasChannelsForTeam = false;
+ for (const channel of Object.values(channels)) {
+ if (channel.team_id === teamId) {
+ // If we have one channel, assume we have all of them
+ hasChannelsForTeam = true;
+ break;
+ }
+ }
+
+ if (!hasChannelsForTeam) {
+ await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
+ }
+ };
+}
+
+export function loadPostsIfNecessary(channel) {
+ return async (dispatch, getState) => {
+ const postsInChannel = getState().entities.posts.postsByChannel[channel.id];
+
+ if (!postsInChannel) {
+ await getPosts(channel.team_id, channel.id)(dispatch, getState);
+ }
+ };
+}
+
+export function selectInitialChannel(teamId) {
+ return async (dispatch, getState) => {
+ const channels = getState().entities.channels.channels;
+
+ for (const channel of Object.values(channels)) {
+ // TODO figure out how to handle when we can't find the town square
+ if (channel.team_id === teamId && channel.name === Constants.DEFAULT_CHANNEL) {
+ await selectChannel(channel.id)(dispatch, getState);
+ break;
+ }
+ }
+ };
+}
diff --git a/app/components/channel_sidebar.js b/app/components/channel_sidebar.js
index e6eb6b88f..99c7f4c7c 100644
--- a/app/components/channel_sidebar.js
+++ b/app/components/channel_sidebar.js
@@ -5,17 +5,19 @@ import React from 'react';
import {Text, View} from 'react-native';
-export default class ChannelSidebar extends React.Component {
+class ChannelSidebar extends React.Component {
static propTypes = {
+ actions: React.PropTypes.shape({
+ selectChannel: React.PropTypes.func.isRequired
+ }).isRequired,
currentTeam: React.PropTypes.object.isRequired,
- channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
- onSelectChannel: React.PropTypes.func
+ channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
};
onSelectChannel = (channel) => {
console.log('clicked channel ' + channel.name); // eslint-disable-line no-console
- // this.props.onSelectChannel(channel);
+ this.props.actions.selectChannel(channel.id);
}
render() {
@@ -45,3 +47,22 @@ export default class ChannelSidebar extends React.Component {
);
}
}
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {selectChannel} from 'service/actions/channels';
+
+function mapStateToProps(state, ownProps) {
+ return ownProps;
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ selectChannel
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChannelSidebar);
diff --git a/app/components/formatted_time.js b/app/components/formatted_time.js
new file mode 100644
index 000000000..1bfccacff
--- /dev/null
+++ b/app/components/formatted_time.js
@@ -0,0 +1,37 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {injectIntl, intlShape} from 'react-intl';
+import {Text} from 'react-native';
+
+class FormattedTime extends React.Component {
+ static propTypes = {
+ intl: intlShape.isRequired,
+ value: React.PropTypes.any.isRequired,
+ format: React.PropTypes.string,
+ children: React.PropTypes.func
+ };
+
+ render() {
+ const {
+ intl,
+ value,
+ children,
+ ...props
+ } = this.props;
+
+ Reflect.deleteProperty(props, 'format');
+
+ const formattedTime = intl.formatTime(value, this.props);
+
+ if (typeof children === 'function') {
+ return children(formattedTime);
+ }
+
+ return {formattedTime};
+ }
+}
+
+export default injectIntl(FormattedTime);
+
diff --git a/app/components/post/index.js b/app/components/post/index.js
new file mode 100644
index 000000000..2b4fe63ff
--- /dev/null
+++ b/app/components/post/index.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Post from './post_container';
+
+export default Post;
diff --git a/app/components/post/post.js b/app/components/post/post.js
new file mode 100644
index 000000000..93af77ef6
--- /dev/null
+++ b/app/components/post/post.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {Text, View} from 'react-native';
+
+import FormattedText from 'app/components/formatted_text';
+import FormattedTime from 'app/components/formatted_time';
+
+import {isSystemMessage} from 'service/utils/post_utils.js';
+
+export default class Post extends React.Component {
+ static propTypes = {
+ post: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object,
+ theme: React.PropTypes.object.isRequired
+ };
+
+ render() {
+ let displayName;
+ if (isSystemMessage(this.props.post)) {
+ displayName = 'System'; // TODO this should be localized
+ } else if (this.props.user) {
+ displayName = this.props.user.username;
+ } else {
+ displayName = (
+
+ );
+ }
+
+ return (
+
+
+ {'['}
+
+ {'] '}
+ {displayName}
+ {': ' + this.props.post.message}
+
+ );
+ }
+}
diff --git a/app/components/post/post_container.js b/app/components/post/post_container.js
new file mode 100644
index 000000000..25b5701fa
--- /dev/null
+++ b/app/components/post/post_container.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {connect} from 'react-redux';
+
+import {getTheme} from 'service/selectors/entities/preferences';
+import {getUser} from 'service/selectors/entities/users';
+
+import Post from './post';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ user: getUser(state, ownProps.post.user_id),
+ post: ownProps.post,
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(Post);
+
diff --git a/app/components/post_list.js b/app/components/post_list.js
new file mode 100644
index 000000000..3a930082f
--- /dev/null
+++ b/app/components/post_list.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+import {ListView} from 'react-native';
+
+import Post from 'app/components/post';
+
+export default class PostList extends React.Component {
+ static propTypes = {
+ posts: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ dataSource: new ListView.DataSource({
+ rowHasChanged: (a, b) => a !== b
+ }).cloneWithRows(this.props.posts)
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({
+ dataSource: this.state.dataSource.cloneWithRows(nextProps.posts)
+ });
+ }
+
+ renderPost(post) {
+ return ;
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/scenes/channel/channel.js b/app/scenes/channel/channel.js
index 20143cf70..522087131 100644
--- a/app/scenes/channel/channel.js
+++ b/app/scenes/channel/channel.js
@@ -2,21 +2,24 @@
// 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 'app/components/channel_sidebar';
-import Loading from 'app/components/loading';
-import RightSidebarMenu from 'app/components/right_sidebar_menu';
import {StatusBar, Text, TouchableHighlight, View} from 'react-native';
+import Drawer from 'react-native-drawer';
+
+import ChannelSidebar from 'app/components/channel_sidebar';
+import RightSidebarMenu from 'app/components/right_sidebar_menu';
+
+import ChannelPostList from './components/channel_post_list';
export default class Channel extends React.Component {
static propTypes = {
- actions: React.PropTypes.object.isRequired,
- currentTeam: React.PropTypes.object,
+ actions: React.PropTypes.shape({
+ loadChannelsIfNecessary: React.PropTypes.func.isRequired,
+ selectInitialChannel: React.PropTypes.func.isRequired
+ }).isRequired,
+ currentTeam: React.PropTypes.object.isRequired,
currentChannel: React.PropTypes.object,
- channels: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
+ channels: React.PropTypes.array,
theme: React.PropTypes.object.isRequired
};
@@ -32,12 +35,16 @@ export default class Channel extends React.Component {
}
componentWillMount() {
- this.props.actions.fetchMyChannelsAndMembers(this.props.currentTeam.id);
+ this.props.actions.loadChannelsIfNecessary(this.props.currentTeam.id).then(() => {
+ return this.props.actions.selectInitialChannel(this.props.currentTeam.id);
+ });
}
componentWillReceiveProps(nextProps) {
- if (this.props.currentTeam && nextProps.currentTeam && this.props.currentTeam.id !== nextProps.currentTeam.id) {
- this.props.actions.fetchMyChannelsAndMembers(nextProps.currentTeam.id);
+ if (this.props.currentTeam.id !== nextProps.currentTeam.id) {
+ this.props.actions.loadChannelsIfNecessary(nextProps.currentTeam.id).then(() => {
+ this.props.actions.selectInitialChannel(nextProps.currentTeam.id);
+ });
}
}
@@ -59,14 +66,16 @@ export default class Channel extends React.Component {
render() {
const {
- currentChannel,
currentTeam,
+ currentChannel,
channels,
theme
} = this.props;
- if (!currentChannel) {
- return ;
+ if (!currentTeam) {
+ return {'Waiting on team'};
+ } else if (!currentChannel) {
+ return {'Waiting on channel'};
}
return (
@@ -109,10 +118,7 @@ export default class Channel extends React.Component {
{'>'}
-
- {currentTeam.id + ' - ' + currentTeam.name}
- {currentChannel.id + ' - ' + currentChannel.name}
-
+
diff --git a/app/scenes/channel/channel_container.js b/app/scenes/channel/channel_container.js
index 20ca5f459..70856aee1 100644
--- a/app/scenes/channel/channel_container.js
+++ b/app/scenes/channel/channel_container.js
@@ -4,38 +4,20 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {fetchMyChannelsAndMembers} from 'service/actions/channels';
+import {loadChannelsIfNecessary, selectInitialChannel} from 'app/actions/views/channel';
+
+import {getChannelsOnCurrentTeam, getCurrentChannel} from 'service/selectors/entities/channels';
import {getTheme} from 'service/selectors/entities/preferences';
+import {getCurrentTeam} from 'service/selectors/entities/teams';
import Channel from './channel.js';
function mapStateToProps(state, ownProps) {
- const currentTeamId = state.entities.teams.currentId;
- const currentTeam = state.entities.teams.teams[currentTeamId];
- let channels = state.entities.channels.channels;
-
- let currentChannel;
- if (ownProps.currentChannelId) {
- currentChannel = state.entities.channels.channels[ownProps.currentChannelId];
- } else {
- // TODO figure out the town square id before this
- for (const channelId of Object.keys(channels)) {
- const channel = channels[channelId];
-
- if (channel.name === 'town-square') {
- currentChannel = channel;
- break;
- }
- }
- }
-
- channels = Object.keys(channels).map((channelId) => channels[channelId]);
-
return {
...ownProps,
- currentTeam,
- currentChannel,
- channels,
+ currentTeam: getCurrentTeam(state),
+ currentChannel: getCurrentChannel(state),
+ channels: getChannelsOnCurrentTeam(state),
theme: getTheme(state)
};
}
@@ -43,7 +25,8 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
- fetchMyChannelsAndMembers
+ loadChannelsIfNecessary,
+ selectInitialChannel
}, dispatch)
};
}
diff --git a/app/scenes/channel/components/channel_post_list/channel_post_list.js b/app/scenes/channel/components/channel_post_list/channel_post_list.js
new file mode 100644
index 000000000..7637ef441
--- /dev/null
+++ b/app/scenes/channel/components/channel_post_list/channel_post_list.js
@@ -0,0 +1,34 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import PostList from 'app/components/post_list';
+
+export default class ChannelPostList extends React.Component {
+ static propTypes = {
+ actions: React.PropTypes.shape({
+ loadPostsIfNecessary: React.PropTypes.func.isRequired
+ }).isRequired,
+ channel: React.PropTypes.object.isRequired,
+ posts: React.PropTypes.array
+ }
+
+ componentDidMount() {
+ this.props.actions.loadPostsIfNecessary(this.props.channel);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.channel.id !== nextProps.channel.id) {
+ this.props.actions.loadPostsIfNecessary(nextProps.channel);
+ }
+ }
+
+ render() {
+ if (!this.props.posts) {
+ return {'waiting on posts'};
+ }
+
+ return ;
+ }
+}
diff --git a/app/scenes/channel/components/channel_post_list/channel_post_list_container.js b/app/scenes/channel/components/channel_post_list/channel_post_list_container.js
new file mode 100644
index 000000000..c184a9c43
--- /dev/null
+++ b/app/scenes/channel/components/channel_post_list/channel_post_list_container.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+import {connect} from 'react-redux';
+
+import {loadPostsIfNecessary} from 'app/actions/views/channel';
+
+import {getPostsInCurrentChannel} from 'service/selectors/entities/posts';
+
+import ChannelPostList from './channel_post_list';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ ...ownProps,
+ posts: getPostsInCurrentChannel(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ loadPostsIfNecessary
+ }, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ChannelPostList);
diff --git a/app/scenes/channel/components/channel_post_list/index.js b/app/scenes/channel/components/channel_post_list/index.js
new file mode 100644
index 000000000..ffb9cfa29
--- /dev/null
+++ b/app/scenes/channel/components/channel_post_list/index.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelPostListContainer from './channel_post_list_container';
+
+export default ChannelPostListContainer;
diff --git a/app/scenes/select_team/select_team.js b/app/scenes/select_team/select_team.js
index 20da299ed..540bfe33d 100644
--- a/app/scenes/select_team/select_team.js
+++ b/app/scenes/select_team/select_team.js
@@ -38,8 +38,9 @@ export default class SelectTeam extends Component {
}
onSelectTeam(team) {
- this.props.actions.selectTeam(team);
- this.props.actions.goToChannelView();
+ this.props.actions.selectTeam(team).then(() => {
+ this.props.actions.goToChannelView();
+ });
}
render() {
diff --git a/package.json b/package.json
index c591b4374..d3ea24189 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"react-redux": "4.4.5",
"redux": "3.6.0",
"redux-batched-actions": "0.1.3",
- "redux-thunk": "2.1.0"
+ "redux-thunk": "2.1.0",
+ "reselect": "2.5.4"
},
"devDependencies": {
"babel-eslint": "7.0.0",
diff --git a/service/actions/channels.js b/service/actions/channels.js
index d85c3eaf1..948778099 100644
--- a/service/actions/channels.js
+++ b/service/actions/channels.js
@@ -15,7 +15,7 @@ import Client from 'service/client';
export function selectChannel(channelId) {
return async (dispatch, getState) => {
dispatch({
- type: ChannelTypes.SELECTED_CHANNEL,
+ type: ChannelTypes.SELECT_CHANNEL,
data: channelId
}, getState);
};
diff --git a/service/actions/posts.js b/service/actions/posts.js
index 00993afc3..6410dc257 100644
--- a/service/actions/posts.js
+++ b/service/actions/posts.js
@@ -128,7 +128,7 @@ export function getPosts(teamId, channelId, offset = 0, limit = Constants.POST_C
} catch (error) {
forceLogoutIfNecessary(error, dispatch);
dispatch({type: PostsTypes.GET_POSTS_FAILURE, error}, getState);
- return;
+ return null;
}
dispatch(batchActions([
@@ -141,6 +141,8 @@ export function getPosts(teamId, channelId, offset = 0, limit = Constants.POST_C
type: PostsTypes.GET_POSTS_SUCCESS
}
]), getState);
+
+ return posts;
};
}
diff --git a/service/actions/websocket.js b/service/actions/websocket.js
index 7efa1bc69..563888dd8 100644
--- a/service/actions/websocket.js
+++ b/service/actions/websocket.js
@@ -233,7 +233,7 @@ function handleLeaveTeamEvent(msg, dispatch, getState) {
data: ''
},
{
- type: ChannelTypes.SELECTED_CHANNEL,
+ type: ChannelTypes.SELECT_CHANNEL,
data: ''
}
]), getState);
@@ -314,7 +314,7 @@ function handleChannelDeletedEvent(msg, dispatch, getState) {
channelId = channel[0];
}
- dispatch({type: ChannelTypes.SELECTED_CHANNEL, data: channelId}, getState);
+ dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channelId}, getState);
}
fetchMyChannelsAndMembers(teams.currentId)(dispatch, getState);
diff --git a/service/constants/channels.js b/service/constants/channels.js
index 01778e402..e19805814 100644
--- a/service/constants/channels.js
+++ b/service/constants/channels.js
@@ -60,7 +60,7 @@ const ChannelTypes = keyMirror({
REMOVE_CHANNEL_MEMBER_SUCCESS: null,
REMOVE_CHANNEL_MEMBER_FAILURE: null,
- SELECTED_CHANNEL: null,
+ SELECT_CHANNEL: null,
LEAVE_CHANNEL: null,
RECEIVED_CHANNEL: null,
RECEIVED_CHANNELS: null,
diff --git a/service/constants/constants.js b/service/constants/constants.js
index 14038f9fc..8f7a0bbea 100644
--- a/service/constants/constants.js
+++ b/service/constants/constants.js
@@ -20,6 +20,7 @@ const Constants = {
DM_CHANNEL: 'D',
POST_DELETED: 'DELETED',
+ SYSTEM_MESSAGE_PREFIX: 'system_',
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show'
};
diff --git a/service/reducers/entities/channels.js b/service/reducers/entities/channels.js
index 4188b2589..2ee895882 100644
--- a/service/reducers/entities/channels.js
+++ b/service/reducers/entities/channels.js
@@ -6,7 +6,7 @@ import {combineReducers} from 'redux';
function currentId(state = '', action) {
switch (action.type) {
- case ChannelTypes.SELECTED_CHANNEL:
+ case ChannelTypes.SELECT_CHANNEL:
return action.data;
case UsersTypes.LOGOUT_SUCCESS:
return '';
diff --git a/service/reducers/entities/posts.js b/service/reducers/entities/posts.js
index 4beeafcc9..64c56c8ea 100644
--- a/service/reducers/entities/posts.js
+++ b/service/reducers/entities/posts.js
@@ -3,13 +3,6 @@
import {Constants, PostsTypes, UsersTypes} from 'service/constants';
-function initialState() {
- return {
- posts: {},
- postsByChannel: {}
- };
-}
-
function handleReceivedPost(posts = {}, postsByChannel = {}, action) {
const post = action.data;
const channelId = post.channel_id;
@@ -130,21 +123,27 @@ function handleRemovePost(posts = {}, postsByChannel = {}, action) {
return {posts: nextPosts, postsByChannel: nextPostsByChannel};
}
-function handlePosts(state = initialState(), action) {
+function handlePosts(posts = {}, postsByChannel = {}, action) {
switch (action.type) {
case PostsTypes.RECEIVED_POST:
- return handleReceivedPost(state.posts, state.postsByChannel, action);
+ return handleReceivedPost(posts, postsByChannel, action);
case PostsTypes.RECEIVED_POSTS:
- return handleReceivedPosts(state.posts, state.postsByChannel, action);
+ return handleReceivedPosts(posts, postsByChannel, action);
case PostsTypes.POST_DELETED:
- return handlePostDeleted(state.posts, state.postsByChannel, action);
+ return handlePostDeleted(posts, postsByChannel, action);
case PostsTypes.REMOVE_POST:
- return handleRemovePost(state.posts, state.postsByChannel, action);
+ return handleRemovePost(posts, postsByChannel, action);
case UsersTypes.LOGOUT_SUCCESS:
- return initialState();
+ return {
+ posts: {},
+ postsByChannel: {}
+ };
default:
- return state;
+ return {
+ posts,
+ postsByChannel
+ };
}
}
@@ -166,8 +165,8 @@ function currentFocusedPostId(state = '', action) {
}
}
-export default function(state = initialState(), action) {
- const {posts, postsByChannel} = handlePosts(state, action);
+export default function(state = {}, action) {
+ const {posts, postsByChannel} = handlePosts(state.posts, state.postsByChannel, action);
const nextState = {
diff --git a/service/reducers/entities/teams.js b/service/reducers/entities/teams.js
index 41e2bd491..abfade978 100644
--- a/service/reducers/entities/teams.js
+++ b/service/reducers/entities/teams.js
@@ -9,6 +9,8 @@ function currentId(state = '', action) {
case TeamsTypes.SELECT_TEAM:
return action.data;
+ case UsersTypes.LOGOUT_SUCCESS:
+ return '';
default:
return state;
}
diff --git a/service/selectors/entities/channels.js b/service/selectors/entities/channels.js
new file mode 100644
index 000000000..02129e6d8
--- /dev/null
+++ b/service/selectors/entities/channels.js
@@ -0,0 +1,38 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {createSelector} from 'reselect';
+
+import {getCurrentTeamId} from 'service/selectors/entities/teams';
+
+function getAllChannels(state) {
+ return state.entities.channels.channels;
+}
+
+export function getCurrentChannelId(state) {
+ return state.entities.channels.currentId;
+}
+
+export const getCurrentChannel = createSelector(
+ getAllChannels,
+ getCurrentChannelId,
+ (allChannels, currentChannelId) => {
+ return allChannels[currentChannelId];
+ }
+);
+
+export const getChannelsOnCurrentTeam = createSelector(
+ getAllChannels,
+ getCurrentTeamId,
+ (allChannels, currentTeamId) => {
+ const channels = [];
+
+ for (const channel of Object.values(allChannels)) {
+ if (channel.team_id === currentTeamId) {
+ channels.push(channel);
+ }
+ }
+
+ return channels;
+ }
+);
diff --git a/service/selectors/entities/posts.js b/service/selectors/entities/posts.js
new file mode 100644
index 000000000..6d3ffdbf3
--- /dev/null
+++ b/service/selectors/entities/posts.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {createSelector} from 'reselect';
+
+function getPosts(state) {
+ return state.entities.posts.posts;
+}
+
+function getPostIdsInCurrentChannel(state) {
+ return state.entities.posts.postsByChannel[state.entities.channels.currentId] || [];
+}
+
+export const getPostsInCurrentChannel = createSelector(
+ getPosts,
+ getPostIdsInCurrentChannel,
+ (posts, postIds) => {
+ return postIds.map((id) => posts[id]);
+ }
+);
diff --git a/service/selectors/entities/teams.js b/service/selectors/entities/teams.js
new file mode 100644
index 000000000..c8a29d3d3
--- /dev/null
+++ b/service/selectors/entities/teams.js
@@ -0,0 +1,21 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {createSelector} from 'reselect';
+
+export function getCurrentTeamId(state) {
+ return state.entities.teams.currentId;
+}
+
+export function getTeams(state) {
+ return state.entities.teams.teams;
+}
+
+export const getCurrentTeam = createSelector(
+ getTeams,
+ getCurrentTeamId,
+ (teams, currentTeamId) => {
+ return teams[currentTeamId];
+ }
+);
+
diff --git a/service/selectors/entities/users.js b/service/selectors/entities/users.js
new file mode 100644
index 000000000..861ee7f3d
--- /dev/null
+++ b/service/selectors/entities/users.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export function getUser(state, id) {
+ return state.entities.users.profiles[id];
+}
diff --git a/service/utils/post_utils.js b/service/utils/post_utils.js
new file mode 100644
index 000000000..7e9f7d9c2
--- /dev/null
+++ b/service/utils/post_utils.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {Constants} from 'service/constants';
+
+export function isSystemMessage(post) {
+ return post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
+}
diff --git a/test/service/actions/channels.test.js b/test/service/actions/channels.test.js
index 15ac1473f..992ec0204 100644
--- a/test/service/actions/channels.test.js
+++ b/test/service/actions/channels.test.js
@@ -25,6 +25,15 @@ describe('Actions.Channels', () => {
await TestHelper.basicClient.logout();
});
+ it('selectChannel', async () => {
+ const channelId = TestHelper.generateId();
+
+ await Actions.selectChannel(channelId)(store.dispatch, store.getState);
+
+ const state = store.getState();
+ assert.equal(state.entities.channels.currentId, channelId);
+ });
+
it('createChannel', async () => {
const channel = {
team_id: TestHelper.basicTeam.id,