From 4ecd3758e75bd304d06fe2dee0b6ded2325db321 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 23 Dec 2016 08:55:33 -0500 Subject: [PATCH] Render posts for the current channel (#153) * Added selectChannel action * Fixed selected team not being cleared on logout * Fixed posts and postsByChannel not being initialized correctly * Added basic post rendering * Renamed selectors files to be plural * Removed empty file * Fixed unit tests after merge * Removing unnecessary code --- app/actions/views/channel.js | 49 +++++++++++++++++++ app/components/channel_sidebar.js | 29 +++++++++-- app/components/formatted_time.js | 37 ++++++++++++++ app/components/post/index.js | 6 +++ app/components/post/post.js | 45 +++++++++++++++++ app/components/post/post_container.js | 20 ++++++++ app/components/post_list.js | 43 ++++++++++++++++ app/scenes/channel/channel.js | 44 ++++++++++------- app/scenes/channel/channel_container.js | 35 ++++--------- .../channel_post_list/channel_post_list.js | 34 +++++++++++++ .../channel_post_list_container.js | 28 +++++++++++ .../components/channel_post_list/index.js | 6 +++ app/scenes/select_team/select_team.js | 5 +- package.json | 3 +- service/actions/channels.js | 2 +- service/actions/posts.js | 4 +- service/actions/websocket.js | 4 +- service/constants/channels.js | 2 +- service/constants/constants.js | 1 + service/reducers/entities/channels.js | 2 +- service/reducers/entities/posts.js | 31 ++++++------ service/reducers/entities/teams.js | 2 + service/selectors/entities/channels.js | 38 ++++++++++++++ service/selectors/entities/posts.js | 20 ++++++++ service/selectors/entities/teams.js | 21 ++++++++ service/selectors/entities/users.js | 6 +++ service/utils/post_utils.js | 8 +++ test/service/actions/channels.test.js | 9 ++++ 28 files changed, 460 insertions(+), 74 deletions(-) create mode 100644 app/actions/views/channel.js create mode 100644 app/components/formatted_time.js create mode 100644 app/components/post/index.js create mode 100644 app/components/post/post.js create mode 100644 app/components/post/post_container.js create mode 100644 app/components/post_list.js create mode 100644 app/scenes/channel/components/channel_post_list/channel_post_list.js create mode 100644 app/scenes/channel/components/channel_post_list/channel_post_list_container.js create mode 100644 app/scenes/channel/components/channel_post_list/index.js create mode 100644 service/selectors/entities/channels.js create mode 100644 service/selectors/entities/posts.js create mode 100644 service/selectors/entities/teams.js create mode 100644 service/selectors/entities/users.js create mode 100644 service/utils/post_utils.js 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,