From c28d64c8dac4c3b7bf193a1c0ec2ed0b177b0fb0 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 2 Feb 2017 11:53:42 -0500 Subject: [PATCH] #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 --- app/components/channel_list/channel_item.js | 2 +- app/components/formatted_date.js | 2 +- app/components/mattermost_icon.js | 43 ++++ app/components/member_list/index.js | 11 +- app/components/member_list/member_list_row.js | 74 +----- app/components/post/post.js | 239 ++++++++++++++++-- app/components/post/post_container.js | 16 +- app/components/post_list.js | 49 ---- app/components/post_list/date_header.js | 57 +++++ app/components/post_list/index.js | 6 + app/components/post_list/post_list.js | 84 ++++++ .../post_list/post_list_container.js | 18 ++ app/components/post_textbox/post_textbox.js | 2 +- app/components/profile_picture/index.js | 6 + .../profile_picture/profile_picture.js | 94 +++++++ .../profile_picture_container.js | 25 ++ .../channel_post_list/channel_post_list.js | 2 +- .../channel_post_list_container.js | 67 ++++- app/utils/{colors.js => theme.js} | 14 + assets/base/i18n/en.json | 1 + service/client/client.js | 9 +- service/selectors/entities/posts.js | 4 +- service/selectors/entities/users.js | 14 +- 23 files changed, 677 insertions(+), 162 deletions(-) create mode 100644 app/components/mattermost_icon.js delete mode 100644 app/components/post_list.js create mode 100644 app/components/post_list/date_header.js create mode 100644 app/components/post_list/index.js create mode 100644 app/components/post_list/post_list.js create mode 100644 app/components/post_list/post_list_container.js create mode 100644 app/components/profile_picture/index.js create mode 100644 app/components/profile_picture/profile_picture.js create mode 100644 app/components/profile_picture/profile_picture_container.js rename app/utils/{colors.js => theme.js} (70%) diff --git a/app/components/channel_list/channel_item.js b/app/components/channel_list/channel_item.js index 130ad5f42..d067ce83b 100644 --- a/app/components/channel_list/channel_item.js +++ b/app/components/channel_list/channel_item.js @@ -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'; diff --git a/app/components/formatted_date.js b/app/components/formatted_date.js index afc4bbefc..15331afd2 100644 --- a/app/components/formatted_date.js +++ b/app/components/formatted_date.js @@ -29,7 +29,7 @@ class FormattedDate extends Component { return children(formattedDate); } - return {formattedDate}; + return {formattedDate}; } } diff --git a/app/components/mattermost_icon.js b/app/components/mattermost_icon.js new file mode 100644 index 000000000..2a10c3385 --- /dev/null +++ b/app/components/mattermost_icon.js @@ -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 ( + + + + + + + + + ); + } +} diff --git a/app/components/member_list/index.js b/app/components/member_list/index.js index 0d8d90e23..6f0acd909 100644 --- a/app/components/member_list/index.js +++ b/app/components/member_list/index.js @@ -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 ( ); diff --git a/app/components/member_list/member_list_row.js b/app/components/member_list/member_list_row.js index 93dee9131..a534d07c6 100644 --- a/app/components/member_list/member_list_row.js +++ b/app/components/member_list/member_list_row.js @@ -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 = ( - - ); - } + const {id, displayName, username, onPress, user} = props; const RowComponent = ( - - - - {StatusComponent} - - + {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; diff --git a/app/components/post/post.js b/app/components/post/post.js index 96791ad31..24dcf8100 100644 --- a/app/components/post/post.js +++ b/app/components/post/post.js @@ -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 = ( - - {'['} - - {'] '} - {displayName} - {': ' + this.props.post.message} + + ); + } + + 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 ; + } + + render() { + const style = getStyleSheet(this.props.theme); + + let profilePicture; + let displayName; + let messageStyle; + if (isSystemMessage(this.props.post)) { + profilePicture = ( + + + + ); + + displayName = ( + + ); + + messageStyle = [style.message, style.systemMessage]; + } else { + profilePicture = ( + + ); + + if (this.props.displayName) { + displayName = this.props.displayName; + } else { + displayName = ( + + ); + } + + messageStyle = style.message; + } + + if (this.props.commentedOnPost) { + return ( + + + {profilePicture} + + + + + {displayName} + + + + + + + {this.renderCommentedOnMessage(style)} + + + {this.renderReplyBar(style)} + + {this.props.post.message} + + + + + ); + } + + return ( + + + {profilePicture} + + {this.renderReplyBar(style)} + + + + {displayName} + + + + + + + + {this.props.post.message} + + + ); } diff --git a/app/components/post/post_container.js b/app/components/post/post_container.js index 25b5701fa..f0aabde87 100644 --- a/app/components/post/post_container.js +++ b/app/components/post/post_container.js @@ -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 }; } diff --git a/app/components/post_list.js b/app/components/post_list.js deleted file mode 100644 index f92fe5673..000000000 --- a/app/components/post_list.js +++ /dev/null @@ -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 ( - - ); - } - - render() { - return ( - - ); - } -} diff --git a/app/components/post_list/date_header.js b/app/components/post_list/date_header.js new file mode 100644 index 000000000..24e09ab8f --- /dev/null +++ b/app/components/post_list/date_header.js @@ -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 ( + + + + + + + + ); + } +} diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js new file mode 100644 index 000000000..6a0053884 --- /dev/null +++ b/app/components/post_list/index.js @@ -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; diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js new file mode 100644 index 000000000..703f3b629 --- /dev/null +++ b/app/components/post_list/post_list.js @@ -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 ( + + ); + } + + renderPost = (post) => { + return ( + + ); + }; + + render() { + return ( + + ); + } +} diff --git a/app/components/post_list/post_list_container.js b/app/components/post_list/post_list_container.js new file mode 100644 index 000000000..ab832e4f7 --- /dev/null +++ b/app/components/post_list/post_list_container.js @@ -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); + diff --git a/app/components/post_textbox/post_textbox.js b/app/components/post_textbox/post_textbox.js index 81c59556b..5b561d38d 100644 --- a/app/components/post_textbox/post_textbox.js +++ b/app/components/post_textbox/post_textbox.js @@ -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'; diff --git a/app/components/profile_picture/index.js b/app/components/profile_picture/index.js new file mode 100644 index 000000000..8619235a0 --- /dev/null +++ b/app/components/profile_picture/index.js @@ -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; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js new file mode 100644 index 000000000..ec6366b05 --- /dev/null +++ b/app/components/profile_picture/profile_picture.js @@ -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 = ( + + + + ); + } + + return ( + + + {status} + + ); + } +} diff --git a/app/components/profile_picture/profile_picture_container.js b/app/components/profile_picture/profile_picture_container.js new file mode 100644 index 000000000..e4599a6e5 --- /dev/null +++ b/app/components/profile_picture/profile_picture_container.js @@ -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); + diff --git a/app/scenes/channel/channel_post_list/channel_post_list.js b/app/scenes/channel/channel_post_list/channel_post_list.js index 7637ef441..d099fa8ed 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list.js +++ b/app/scenes/channel/channel_post_list/channel_post_list.js @@ -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() { diff --git a/app/scenes/channel/channel_post_list/channel_post_list_container.js b/app/scenes/channel/channel_post_list/channel_post_list_container.js index c184a9c43..b379db35a 100644 --- a/app/scenes/channel/channel_post_list/channel_post_list_container.js +++ b/app/scenes/channel/channel_post_list/channel_post_list_container.js @@ -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) }; } diff --git a/app/utils/colors.js b/app/utils/theme.js similarity index 70% rename from app/utils/colors.js rename to app/utils/theme.js index 0d84e9849..f46af4f18 100644 --- a/app/utils/colors.js +++ b/app/utils/theme.js @@ -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] === '#') { diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 908fa1f82..739ee2b29 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/service/client/client.js b/service/client/client.js index 5207f81b9..c2a412c32 100644 --- a/service/client/client.js +++ b/service/client/client.js @@ -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 diff --git a/service/selectors/entities/posts.js b/service/selectors/entities/posts.js index 6d3ffdbf3..1e5fc88d1 100644 --- a/service/selectors/entities/posts.js +++ b/service/selectors/entities/posts.js @@ -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]); diff --git a/service/selectors/entities/users.js b/service/selectors/entities/users.js index db5d56d06..8c2861894 100644 --- a/service/selectors/entities/users.js +++ b/service/selectors/entities/users.js @@ -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]; +}