#96/#112/PLT-5370/PLT-5372 Styling posts and channel view (#213)

* Updated post rendering in center channel to include dates, display names, themeing, profile pictures, and threads

* Added status to ProfilePicture component

* Changed MemberListRow to use a ProfilePicture

* Removed unused prop

* Fixed incorrect copyright year on new file

* Removed unnecessary ref
This commit is contained in:
Harrison Healey 2017-02-02 11:53:42 -05:00 committed by enahum
parent 71b2ecd394
commit c28d64c8da
23 changed files with 677 additions and 162 deletions

View file

@ -6,7 +6,7 @@ import {TouchableHighlight, Text, View} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {changeOpacity} from 'app/utils/colors';
import {changeOpacity} from 'app/utils/theme';
import {Constants} from 'service/constants';

View file

@ -29,7 +29,7 @@ class FormattedDate extends Component {
return children(formattedDate);
}
return <Text>{formattedDate}</Text>;
return <Text {...props}>{formattedDate}</Text>;
}
}

View file

@ -0,0 +1,43 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import Svg, {
G,
Path
} from 'react-native-svg';
export default class AwayStatus extends React.Component {
static propTypes = {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
color: React.PropTypes.string.isRequired
};
render() {
return (
<Svg
height={this.props.height}
width={this.props.width}
viewBox='0 0 500 500'
>
<G id='XMLID_1_'>
<G id='XMLID_3_'>
<Path
id='XMLID_4_'
class='st0'
d='M396.9,47.7l2.6,53.1c43,47.5,60,114.8,38.6,178.1c-32,94.4-137.4,144.1-235.4,110.9 S51.1,253.1,83,158.7C104.5,95.2,159.2,52,222.5,40.5l34.2-40.4C150-2.8,49.3,63.4,13.3,169.9C-31,300.6,39.1,442.5,169.9,486.7 s272.6-25.8,316.9-156.6C522.7,223.9,483.1,110.3,396.9,47.7z'
fill={this.props.color}
/>
</G>
<Path
id='XMLID_2_'
class='st0'
d='M335.6,204.3l-1.8-74.2l-1.5-42.7l-1-37c0,0,0.2-17.8-0.4-22c-0.1-0.9-0.4-1.6-0.7-2.2 c0-0.1-0.1-0.2-0.1-0.3c0-0.1-0.1-0.2-0.1-0.2c-0.7-1.2-1.8-2.1-3.1-2.6c-1.4-0.5-2.9-0.4-4.2,0.2c0,0-0.1,0-0.1,0 c-0.2,0.1-0.3,0.1-0.4,0.2c-0.6,0.3-1.2,0.7-1.8,1.3c-3,3-13.7,17.2-13.7,17.2l-23.2,28.8l-27.1,33l-46.5,57.8 c0,0-21.3,26.6-16.6,59.4s29.1,48.7,48,55.1c18.9,6.4,48,8.5,71.6-14.7C336.4,238.4,335.6,204.3,335.6,204.3z'
fill={this.props.color}
/>
</G>
</Svg>
);
}
}

View file

@ -9,7 +9,6 @@ import {
View
} from 'react-native';
import Client from 'service/client';
import {displayUsername} from 'service/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
@ -110,18 +109,16 @@ export default class MemberList extends PureComponent {
);
}
renderRow = (data) => {
const {id, username, status} = data;
const displayName = displayUsername(data, this.props.preferences);
const pictureURL = Client.getProfilePictureUrl(data.id);
renderRow = (user) => {
const {id, username} = user;
const displayName = displayUsername(user, this.props.preferences);
return (
<MemberListRow
id={id}
pictureURL={pictureURL}
user={user}
displayName={displayName}
username={username}
status={status}
onPress={this.props.onRowPress}
/>
);

View file

@ -1,30 +1,14 @@
import React, {PropTypes} from 'react';
import {
Image,
StyleSheet,
Text,
TouchableHighlight,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
const placeholder = require('assets/images/profile.jpg');
import ProfilePicture from 'app/components/profile_picture';
const style = StyleSheet.create({
avatar: {
height: 40,
width: 40,
borderRadius: 20
},
avatarContainer: {
height: 50,
width: 50,
alignItems: 'center',
justifyContent: 'center'
},
away: {
backgroundColor: '#d3b141'
},
container: {
flexDirection: 'row',
padding: 10,
@ -34,25 +18,6 @@ const style = StyleSheet.create({
displayName: {
fontSize: 16
},
offline: {
backgroundColor: 'white',
borderColor: '#bababa'
},
online: {
backgroundColor: 'green'
},
statusContainer: {
width: 16,
height: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 5,
right: 5
},
textContainer: {
flex: 1,
flexDirection: 'row',
@ -74,36 +39,14 @@ function createTouchableComponent(children, action) {
}
function MemberListRow(props) {
const {id, displayName, pictureURL, username, status, onPress} = props;
const statusToIcon = {
away: 'minus',
online: 'check'
};
let StatusComponent = null;
if (statusToIcon[status]) {
StatusComponent = (
<Icon
name={statusToIcon[status]}
size={10}
color='#fff'
/>
);
}
const {id, displayName, username, onPress, user} = props;
const RowComponent = (
<View style={style.container}>
<View style={style.avatarContainer}>
<Image
style={style.avatar}
source={{uri: pictureURL}}
defaultSource={placeholder}
/>
<View style={[style.statusContainer, style[status]]}>
{StatusComponent}
</View>
</View>
<ProfilePicture
user={user}
size={40}
/>
<View style={style.textContainer}>
<Text style={style.displayName}>
{displayName}
@ -126,13 +69,8 @@ MemberListRow.propTypes = {
id: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
pictureURL: PropTypes.string,
status: PropTypes.string,
username: PropTypes.string.isRequired,
onPress: PropTypes.func
};
MemberListRow.defaultProps = {
status: 'offline'
};
export default MemberListRow;

View file

@ -2,29 +2,109 @@
// See License.txt for license information.
import React from 'react';
import {Text, View} from 'react-native';
import {
StyleSheet,
Text,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import MattermostIcon from 'app/components/mattermost_icon';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {isSystemMessage} from 'service/utils/post_utils.js';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row'
},
rightColumn: {
flexGrow: 1,
flexDirection: 'column',
marginRight: 12
},
postInfoContainer: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 10
},
messageContainerWithReplyBar: {
flexDirection: 'row'
},
profilePictureContainer: {
marginBottom: 10,
marginRight: 10,
marginLeft: 12,
marginTop: 10
},
replyBar: {
backgroundColor: theme.centerChannelColor,
opacity: 0.1,
marginRight: 10,
width: 3
},
replyBarFirst: {
marginTop: 10
},
replyBarLast: {
marginBottom: 10
},
displayName: {
fontSize: 14,
fontWeight: '600',
marginRight: 10,
color: theme.centerChannelColor
},
time: {
color: theme.centerChannelColor,
fontSize: 12,
opacity: 0.5
},
commentedOn: {
color: theme.centerChannelColor,
lineHeight: 21
},
message: {
color: theme.centerChannelColor,
fontSize: 14,
lineHeight: 21,
marginBottom: 10
},
systemMessage: {
opacity: 0.5
}
});
});
export default class Post extends React.Component {
static propTypes = {
style: React.PropTypes.object,
style: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number]),
post: React.PropTypes.object.isRequired,
user: React.PropTypes.object,
displayName: React.PropTypes.string,
isFirstReply: React.PropTypes.bool,
isLastReply: React.PropTypes.bool,
commentedOnPost: React.PropTypes.object,
commentedOnDisplayName: React.PropTypes.string,
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;
renderCommentedOnMessage = (style) => {
if (!this.props.commentedOnPost) {
return null;
}
const displayName = this.props.commentedOnDisplayName;
let name;
if (displayName) {
name = displayName;
} else {
displayName = (
name = (
<FormattedText
id='channel_loader.someone'
defaultMessage='Someone'
@ -32,14 +112,141 @@ export default class Post extends React.Component {
);
}
let apostrophe;
if (displayName && displayName.slice(-1) === 's') {
apostrophe = '\'';
} else {
apostrophe = '\'s';
}
return (
<View style={this.props.style}>
<Text>
{'['}
<FormattedTime value={this.props.post.create_at}/>
{'] '}
{displayName}
{': ' + this.props.post.message}</Text>
<FormattedText
id='post_body.commentedOn'
defaultMessage='Commented on {name}{apostrophe} message: '
values={{
name,
apostrophe
}}
style={style.commentedOn}
/>
);
}
renderReplyBar = (style) => {
if (!this.props.post.root_id) {
return null;
}
const replyBarStyle = [style.replyBar];
if (this.props.isFirstReply && !this.props.commentedOnPost) {
replyBarStyle.push(style.replyBarFirst);
}
if (this.props.isLastReply) {
replyBarStyle.push(style.replyBarLast);
}
return <View style={replyBarStyle}/>;
}
render() {
const style = getStyleSheet(this.props.theme);
let profilePicture;
let displayName;
let messageStyle;
if (isSystemMessage(this.props.post)) {
profilePicture = (
<View style={style.profilePicture}>
<MattermostIcon
color={this.props.theme.centerChannelColor}
height={32}
width={32}
/>
</View>
);
displayName = (
<FormattedText
id='post_info.system'
defaultMessage='System'
/>
);
messageStyle = [style.message, style.systemMessage];
} else {
profilePicture = (
<ProfilePicture
user={this.props.user}
size={32}
/>
);
if (this.props.displayName) {
displayName = this.props.displayName;
} else {
displayName = (
<FormattedText
id='channel_loader.someone'
defaultMessage='Someone'
/>
);
}
messageStyle = style.message;
}
if (this.props.commentedOnPost) {
return (
<View style={[style.container, this.props.style]}>
<View style={style.profilePictureContainer}>
{profilePicture}
</View>
<View style={style.rightColumn}>
<View style={style.postInfoContainer}>
<Text style={style.displayName}>
{displayName}
</Text>
<Text style={style.time}>
<FormattedTime value={this.props.post.create_at}/>
</Text>
</View>
<View>
{this.renderCommentedOnMessage(style)}
</View>
<View style={style.messageContainerWithReplyBar}>
{this.renderReplyBar(style)}
<Text style={messageStyle}>
{this.props.post.message}
</Text>
</View>
</View>
</View>
);
}
return (
<View style={[style.container, this.props.style]}>
<View style={style.profilePictureContainer}>
{profilePicture}
</View>
{this.renderReplyBar(style)}
<View style={style.rightColumn}>
<View style={style.postInfoContainer}>
<Text style={style.displayName}>
{displayName}
</Text>
<Text style={style.time}>
<FormattedTime value={this.props.post.create_at}/>
</Text>
</View>
<View style={style.messageContainer}>
<Text style={messageStyle}>
{this.props.post.message}
</Text>
</View>
</View>
</View>
);
}

View file

@ -3,16 +3,24 @@
import {connect} from 'react-redux';
import {getTheme} from 'service/selectors/entities/preferences';
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
import {getUser} from 'service/selectors/entities/users';
import {displayUsername} from 'service/utils/user_utils';
import Post from './post';
function mapStateToProps(state, ownProps) {
const user = getUser(state, ownProps.post.user_id);
const commentedOnUser = ownProps.commentedOnPost ? getUser(state, ownProps.commentedOnPost.user_id) : null;
const myPreferences = getMyPreferences(state);
return {
user: getUser(state, ownProps.post.user_id),
post: ownProps.post,
theme: getTheme(state)
user,
displayName: displayUsername(user, myPreferences),
commentedOnDisplayName: displayUsername(commentedOnUser, myPreferences),
theme: getTheme(state),
...ownProps
};
}

View file

@ -1,49 +0,0 @@
// 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 (
<Post
style={{transform: [{rotate: '180deg'}]}}
post={post}
/>
);
}
render() {
return (
<ListView
style={{transform: [{rotate: '180deg'}]}}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderRow={this.renderPost}
/>
);
}
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {StyleSheet, View} from 'react-native';
import FormattedDate from 'app/components/formatted_date';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row'
},
dateContainer: {
marginLeft: 15,
marginRight: 15
},
line: {
backgroundColor: theme.centerChannelColor,
flex: 1,
height: StyleSheet.hairlineWidth,
opacity: 0.2
},
date: {
color: theme.centerChannelColor,
fontSize: 14,
fontWeight: '600'
}
});
});
export default class DateHeader extends React.Component {
static propTypes = {
date: React.PropTypes.object.isRequired,
theme: React.PropTypes.object.isRequired,
style: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.number])
};
render() {
const style = getStyleSheet(this.props.theme);
return (
<View style={[style.container, this.props.style]}>
<View style={style.line}/>
<View style={style.dateContainer}>
<FormattedDate
style={style.date}
value={this.props.date}
/>
</View>
<View style={style.line}/>
</View>
);
}
}

View file

@ -0,0 +1,6 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PostList from './post_list_container';
export default PostList;

View file

@ -0,0 +1,84 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {ListView, StyleSheet} from 'react-native';
import Post from 'app/components/post';
import DateHeader from './date_header';
const style = StyleSheet.create({
container: {
transform: [{rotate: '180deg'}]
},
row: {
transform: [{rotate: '180deg'}]
}
});
export default class PostList extends React.Component {
static propTypes = {
posts: React.PropTypes.object.isRequired,
theme: 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.cloneWithRowsAndSections(nextProps.posts)
});
}
renderRow = (row) => {
if (row instanceof Date) {
return this.renderDateHeader(row);
}
return this.renderPost(row);
}
renderDateHeader = (date) => {
return (
<DateHeader
theme={this.props.theme}
style={style.row}
date={date}
/>
);
}
renderPost = (post) => {
return (
<Post
style={style.row}
post={post}
isFirstReply={post.isFirstReply}
isLastReply={post.isLastReply}
commentedOnPost={post.commentedOnPost}
/>
);
};
render() {
return (
<ListView
style={style.container}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
showsVerticalScrollIndicator={false}
/>
);
}
}

View file

@ -0,0 +1,18 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'service/selectors/entities/preferences';
import PostList from './post_list';
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(PostList);

View file

@ -6,7 +6,7 @@ import {TouchableHighlight, View} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity} from 'app/utils/colors';
import {changeOpacity} from 'app/utils/theme';
// import PaperClipIcon from './components/paper_clip_icon.js';

View file

@ -0,0 +1,6 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ProfilePicture from './profile_picture_container';
export default ProfilePicture;

View file

@ -0,0 +1,94 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {Image, StyleSheet, View} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import placeholder from 'assets/images/profile.jpg';
import Client from 'service/client';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
alignItems: 'flex-end',
justifyContent: 'flex-end'
},
statusContainer: {
width: 14,
height: 14,
position: 'absolute',
bottom: 0,
right: 0,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
borderWidth: 1.5,
borderColor: theme.centerChannelBg
},
status: {
backgroundColor: 'transparent',
color: theme.centerChannelBg
},
online: {
backgroundColor: theme.onlineIndicator
},
away: {
backgroundColor: theme.awayIndicator
},
offline: {
backgroundColor: theme.centerChannelBg,
borderColor: '#bababa'
}
});
});
const statusToIcon = {
online: 'check',
away: 'minus'
};
export default class ProfilePicture extends React.PureComponent {
static propTypes = {
size: React.PropTypes.number.isRequired,
user: React.PropTypes.object,
status: React.PropTypes.string,
theme: React.PropTypes.object.isRequired
};
render() {
const style = getStyleSheet(this.props.theme);
let pictureUrl;
if (this.props.user) {
pictureUrl = Client.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
}
let status;
if (this.props.status && statusToIcon[this.props.status]) {
status = (
<View style={[style.statusContainer, style[this.props.status]]}>
<Icon
style={style.status}
name={statusToIcon[this.props.status]}
size={8}
/>
</View>
);
}
return (
<View style={{width: this.props.size, height: this.props.size}}>
<Image
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
source={{uri: pictureUrl}}
defaultSource={placeholder}
/>
{status}
</View>
);
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'service/selectors/entities/preferences';
import {getStatusForUserId} from 'service/selectors/entities/users';
import ProfilePicture from './profile_picture';
function mapStateToProps(state, ownProps) {
let status = ownProps.status;
if (!status && ownProps.user) {
status = ownProps.user.status || getStatusForUserId(state, ownProps.user.id);
}
return {
theme: ownProps.theme || getTheme(state),
status,
...ownProps
};
}
export default connect(mapStateToProps)(ProfilePicture);

View file

@ -11,7 +11,7 @@ export default class ChannelPostList extends React.Component {
loadPostsIfNecessary: React.PropTypes.func.isRequired
}).isRequired,
channel: React.PropTypes.object.isRequired,
posts: React.PropTypes.array
posts: React.PropTypes.object.isRequired
}
componentDidMount() {

View file

@ -3,17 +3,80 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {loadPostsIfNecessary} from 'app/actions/views/channel';
import {getPostsInCurrentChannel} from 'service/selectors/entities/posts';
import {getAllPosts, getPostsInCurrentChannel} from 'service/selectors/entities/posts';
import ChannelPostList from './channel_post_list';
const getPostsInCurrentChannelGroupedByDay = createSelector(
getPostsInCurrentChannel,
getAllPosts,
(postsInChannel, allPosts) => {
const postsByDay = {};
for (let i = 0; i < postsInChannel.length; i++) {
let post = postsInChannel[i];
const dateString = new Date(post.create_at).toDateString();
if (!postsByDay[dateString]) {
postsByDay[dateString] = [];
}
if (post.root_id) {
let isFirstReply = false;
let isLastReply = false;
let commentedOnPost;
if (i + 1 <= postsInChannel.length) {
const previousPost = postsInChannel[i + 1];
if (previousPost.root_id !== post.root_id) {
isFirstReply = true;
if (previousPost.id !== post.root_id) {
commentedOnPost = allPosts[post.root_id];
}
}
} else {
// The first visible comment will always be the first comment in a thread and will be
// commenting on a post that isn't visible
isFirstReply = true;
commentedOnPost = allPosts[post.root_id];
}
if (i - 1 < 0 || postsInChannel[i - 1].root_id !== post.root_id) {
isLastReply = true;
}
post = {
...post,
isFirstReply,
isLastReply,
commentedOnPost
};
}
postsByDay[dateString].push(post);
}
// Push the date on after the posts so that it's rendered first
for (const dateString in postsByDay) {
if (postsByDay.hasOwnProperty(dateString)) {
postsByDay[dateString].push(new Date(dateString));
}
}
return postsByDay;
}
);
function mapStateToProps(state, ownProps) {
return {
...ownProps,
posts: getPostsInCurrentChannel(state)
posts: getPostsInCurrentChannelGroupedByDay(state)
};
}

View file

@ -1,6 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export function makeStyleSheetFromTheme(getStyleFromTheme) {
let lastTheme = null;
let style = null;
return (theme) => {
if (theme !== lastTheme) {
style = getStyleFromTheme(theme);
lastTheme = theme;
}
return style;
};
}
export function changeOpacity(oldColor, opacity) {
let color = oldColor;
if (color[0] === '#') {

View file

@ -1584,6 +1584,7 @@
"post_info.mobile.unflag": "Unflag",
"post_info.permalink": "Permalink",
"post_info.reply": "Reply",
"post_info.system": "System",
"posts_view.loadMore": "Load more messages",
"posts_view.newMsg": "New Messages",
"removed_channel.channelName": "the channel",

View file

@ -342,8 +342,13 @@ export default class Client {
);
};
getProfilePictureUrl = (userId) => {
return `${this.getUsersRoute()}/${userId}/image`;
getProfilePictureUrl = (userId, lastPictureUpdate) => {
let params = '';
if (lastPictureUpdate) {
params = `?time=${lastPictureUpdate}`;
}
return `${this.getUsersRoute()}/${userId}/image${params}`;
};
// Team routes

View file

@ -3,7 +3,7 @@
import {createSelector} from 'reselect';
function getPosts(state) {
export function getAllPosts(state) {
return state.entities.posts.posts;
}
@ -12,7 +12,7 @@ function getPostIdsInCurrentChannel(state) {
}
export const getPostsInCurrentChannel = createSelector(
getPosts,
getAllPosts,
getPostIdsInCurrentChannel,
(posts, postIds) => {
return postIds.map((id) => posts[id]);

View file

@ -4,7 +4,6 @@
import {createSelector} from 'reselect';
import {getCurrentChannelId} from './channels';
import {getMyPreferences} from './preferences';
export function getCurrentUserId(state) {
return state.entities.users.currentId;
@ -44,20 +43,15 @@ export const getProfileSetInCurrentChannel = createSelector(
export const getProfilesInCurrentChannel = createSelector(
getUsers,
getUserStatuses,
getProfileSetInCurrentChannel,
getMyPreferences,
(profiles, statuses, currentChannelProfileSet) => {
(profiles, currentChannelProfileSet) => {
const currentProfiles = [];
if (typeof currentChannelProfileSet === 'undefined') {
return currentProfiles;
}
currentChannelProfileSet.forEach((p) => {
currentProfiles.push({
...profiles[p],
status: statuses[p]
});
currentProfiles.push(profiles[p]);
});
const sortedCurrentProfiles = currentProfiles.sort((a, b) => {
@ -70,3 +64,7 @@ export const getProfilesInCurrentChannel = createSelector(
return sortedCurrentProfiles;
}
);
export function getStatusForUserId(state, userId) {
return getUserStatuses(state)[userId];
}