#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:
parent
71b2ecd394
commit
c28d64c8da
23 changed files with 677 additions and 162 deletions
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class FormattedDate extends Component {
|
|||
return children(formattedDate);
|
||||
}
|
||||
|
||||
return <Text>{formattedDate}</Text>;
|
||||
return <Text {...props}>{formattedDate}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
43
app/components/mattermost_icon.js
Normal file
43
app/components/mattermost_icon.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
57
app/components/post_list/date_header.js
Normal file
57
app/components/post_list/date_header.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
6
app/components/post_list/index.js
Normal file
6
app/components/post_list/index.js
Normal 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;
|
||||
84
app/components/post_list/post_list.js
Normal file
84
app/components/post_list/post_list.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
18
app/components/post_list/post_list_container.js
Normal file
18
app/components/post_list/post_list_container.js
Normal 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);
|
||||
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
6
app/components/profile_picture/index.js
Normal file
6
app/components/profile_picture/index.js
Normal 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;
|
||||
94
app/components/profile_picture/profile_picture.js
Normal file
94
app/components/profile_picture/profile_picture.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
25
app/components/profile_picture/profile_picture_container.js
Normal file
25
app/components/profile_picture/profile_picture_container.js
Normal 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);
|
||||
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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] === '#') {
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue