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];
+}