diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js
index d71f63636..e1985e794 100644
--- a/app/actions/navigation/index.js
+++ b/app/actions/navigation/index.js
@@ -102,6 +102,20 @@ export function goToThread(channelId, rootId) {
};
}
+export function goToUserProfile(userId) {
+ return async (dispatch, getState) => {
+ dispatch({
+ type: NavigationTypes.NAVIGATION_PUSH,
+ route: {
+ ...Routes.UserProfile,
+ props: {
+ userId
+ }
+ }
+ }, getState);
+ };
+}
+
export function openChannelDrawer() {
return async (dispatch, getState) => {
dispatch({
diff --git a/app/actions/views/user_profile.js b/app/actions/views/user_profile.js
new file mode 100644
index 000000000..253120171
--- /dev/null
+++ b/app/actions/views/user_profile.js
@@ -0,0 +1,12 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {makeDirectChannel} from 'app/actions/views/more_dms';
+import {NavigationTypes} from 'app/constants';
+
+export function handleSendMessage(otherUserId) {
+ return async (dispatch, getState) => {
+ await makeDirectChannel(otherUserId)(dispatch, getState);
+ dispatch({type: NavigationTypes.NAVIGATION_POP_TO_INDEX, index: 0}, getState);
+ };
+}
diff --git a/app/components/post/post.js b/app/components/post/post.js
index 55f7636a0..8ae8221e0 100644
--- a/app/components/post/post.js
+++ b/app/components/post/post.js
@@ -1,12 +1,13 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import React from 'react';
+import React, {Component, PropTypes} from 'react';
import {
Image,
StyleSheet,
Text,
TouchableHighlight,
+ TouchableOpacity,
View
} from 'react-native';
@@ -89,19 +90,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
});
});
-export default class Post extends React.Component {
+export default class Post extends Component {
static propTypes = {
style: View.propTypes.style,
- post: React.PropTypes.object.isRequired,
- user: React.PropTypes.object,
- displayName: React.PropTypes.string,
- renderReplies: React.PropTypes.bool,
- isFirstReply: React.PropTypes.bool,
- isLastReply: React.PropTypes.bool,
- commentedOnPost: React.PropTypes.object,
- commentedOnDisplayName: React.PropTypes.string,
- theme: React.PropTypes.object.isRequired,
- onPress: React.PropTypes.func
+ post: PropTypes.object.isRequired,
+ user: PropTypes.object,
+ displayName: PropTypes.string,
+ renderReplies: PropTypes.bool,
+ isFirstReply: PropTypes.bool,
+ isLastReply: PropTypes.bool,
+ commentedOnPost: PropTypes.object,
+ commentedOnDisplayName: PropTypes.string,
+ theme: PropTypes.object.isRequired,
+ onPress: PropTypes.func,
+ actions: PropTypes.shape({
+ goToUserProfile: PropTypes.func.isRequired
+ }).isRequired
};
handlePress = () => {
@@ -177,6 +181,10 @@ export default class Post extends React.Component {
return attachments;
}
+ viewUserProfile = () => {
+ this.props.actions.goToUserProfile(this.props.user.id);
+ }
+
render() {
const style = getStyleSheet(this.props.theme);
const PROFILE_PICTURE_SIZE = 32;
@@ -199,6 +207,7 @@ export default class Post extends React.Component {
);
@@ -217,23 +226,36 @@ export default class Post extends React.Component {
);
- displayName = this.props.post.props.override_username;
+ displayName = (
+
+ {this.props.post.props.override_username}
+
+ );
messageStyle = [style.message, style.webhookMessage];
} else {
profilePicture = (
-
+
+
+
);
if (this.props.displayName) {
- displayName = this.props.displayName;
+ displayName = (
+
+
+ {this.props.displayName}
+
+
+ );
} else {
displayName = (
);
}
@@ -250,9 +272,7 @@ export default class Post extends React.Component {
-
- {displayName}
-
+ {displayName}
@@ -279,9 +299,7 @@ export default class Post extends React.Component {
{this.renderReplyBar(style)}
-
- {displayName}
-
+ {displayName}
diff --git a/app/components/post/post_container.js b/app/components/post/post_container.js
index f0aabde87..f51fbd0e8 100644
--- a/app/components/post/post_container.js
+++ b/app/components/post/post_container.js
@@ -2,8 +2,10 @@
// See License.txt for license information.
import {connect} from 'react-redux';
+import {bindActionCreators} from 'redux';
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
+import {goToUserProfile} from 'app/actions/navigation';
import {getUser} from 'service/selectors/entities/users';
import {displayUsername} from 'service/utils/user_utils';
@@ -24,5 +26,12 @@ function mapStateToProps(state, ownProps) {
};
}
-export default connect(mapStateToProps)(Post);
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ goToUserProfile
+ }, dispatch)
+ };
+}
+export default connect(mapStateToProps, mapDispatchToProps)(Post);
diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js
index 6d9ddb352..c070d2538 100644
--- a/app/components/profile_picture/profile_picture.js
+++ b/app/components/profile_picture/profile_picture.js
@@ -13,24 +13,21 @@ import Client from 'service/client';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
- container: {
- alignItems: 'flex-end',
- justifyContent: 'flex-end'
- },
- statusContainer: {
- width: 14,
- height: 14,
+ statusWrapper: {
position: 'absolute',
bottom: 0,
right: 0,
+ overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
- borderRadius: 8,
- borderWidth: 1.5,
borderColor: theme.centerChannelBg
},
+ statusContainer: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ overflow: 'hidden'
+ },
status: {
- backgroundColor: 'transparent',
color: theme.centerChannelBg
},
online: {
@@ -54,6 +51,9 @@ const statusToIcon = {
export default class ProfilePicture extends React.PureComponent {
static propTypes = {
size: React.PropTypes.number,
+ statusBorderWidth: React.PropTypes.number,
+ statusSize: React.PropTypes.number,
+ statusIconSize: React.PropTypes.number,
user: React.PropTypes.object,
status: React.PropTypes.string,
theme: React.PropTypes.object.isRequired,
@@ -63,7 +63,10 @@ export default class ProfilePicture extends React.PureComponent {
};
static defaultProps = {
- size: 128
+ size: 128,
+ statusBorderWidth: 2,
+ statusSize: 14,
+ statusIconSize: 8
};
componentDidMount() {
@@ -86,7 +89,7 @@ export default class ProfilePicture extends React.PureComponent {
);
}
@@ -99,8 +102,32 @@ export default class ProfilePicture extends React.PureComponent {
defaultSource={placeholder}
/>
{this.props.status &&
-
- {statusIcon}
+
+
+ {statusIcon}
+
}
diff --git a/app/constants/navigation.js b/app/constants/navigation.js
index d0a28f2c1..0d980e2ec 100644
--- a/app/constants/navigation.js
+++ b/app/constants/navigation.js
@@ -6,6 +6,7 @@ import keyMirror from 'service/utils/key_mirror';
const NavigationTypes = keyMirror({
NAVIGATION_PUSH: null,
NAVIGATION_POP: null,
+ NAVIGATION_POP_TO_INDEX: null,
NAVIGATION_OPEN_LEFT_DRAWER: null,
NAVIGATION_OPEN_RIGHT_DRAWER: null,
NAVIGATION_CLOSE_DRAWERS: null,
diff --git a/app/navigation/routes.js b/app/navigation/routes.js
index 24bc821ab..2b86be46a 100644
--- a/app/navigation/routes.js
+++ b/app/navigation/routes.js
@@ -18,7 +18,8 @@ import {
Search,
SelectServer,
SelectTeam,
- Thread
+ Thread,
+ UserProfile
} from 'app/scenes';
import keyMirror from 'service/utils/key_mirror';
@@ -136,6 +137,14 @@ export const Routes = {
navigationProps: {
title: {id: 'mobile.routes.thread', defaultMessage: 'Thread'}
}
+ },
+ UserProfile: {
+ key: 'UserProfile',
+ transition: RouteTransitions.Horizontal,
+ component: UserProfile,
+ navigationProps: {
+ title: {id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}
+ }
}
};
diff --git a/app/reducers/navigation/index.js b/app/reducers/navigation/index.js
index 979fd6eee..9a281a44c 100644
--- a/app/reducers/navigation/index.js
+++ b/app/reducers/navigation/index.js
@@ -59,6 +59,41 @@ export default function(state = initialState, action) {
return NavigationExperimental.StateUtils.pop(state);
+ case NavigationTypes.NAVIGATION_POP_TO_INDEX: {
+ let newState = {...state};
+
+ if (!newState.isModal && (newState.leftDrawerOpen || newState.rightDrawerOpen)) {
+ newState = {
+ ...newState,
+ leftDrawerOpen: false,
+ rightDrawerOpen: false
+ };
+ }
+
+ if (newState.isModal) {
+ newState = {
+ ...newState,
+ modal: {
+ index: 0,
+ routes: []
+ },
+ isModal: false
+ };
+ }
+
+ if (action.index === newState.index || action.index > newState.routes.length - 1) {
+ return newState;
+ }
+
+ const nextRoutes = newState.routes.slice(0, action.index + 1);
+
+ return {
+ ...newState,
+ index: action.index,
+ routes: nextRoutes
+ };
+ }
+
case NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER:
return {
...state,
diff --git a/app/scenes/index.js b/app/scenes/index.js
index a6bc7347a..9946c33ad 100644
--- a/app/scenes/index.js
+++ b/app/scenes/index.js
@@ -18,6 +18,7 @@ import Search from './search/search_container.js';
import SelectServer from './select_server/select_server_container.js';
import SelectTeam from './select_team/select_team_container.js';
import Thread from './thread';
+import UserProfile from './user_profile';
module.exports = {
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
@@ -36,5 +37,6 @@ module.exports = {
Search,
SelectServer,
SelectTeam,
- Thread
+ Thread,
+ UserProfile
};
diff --git a/app/scenes/navigationSceneConnect.js b/app/scenes/navigationSceneConnect.js
index 485689c7f..964005fe7 100644
--- a/app/scenes/navigationSceneConnect.js
+++ b/app/scenes/navigationSceneConnect.js
@@ -18,12 +18,12 @@ const defaults = {
renderBackButton: (props, emitter, theme) => {
return (
{
+ return StyleSheet.create({
+ container: {
+ flex: 1
+ },
+ content: {
+ marginBottom: 25,
+ marginHorizontal: 15
+ },
+ displayName: {
+ marginTop: 15,
+ color: theme.centerChannelColor,
+ fontSize: 17,
+ fontWeight: '600'
+ },
+ header: {
+ fontSize: 13,
+ fontWeight: '600',
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ marginTop: 25,
+ marginBottom: 10
+ },
+ scrollView: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg
+ },
+ scrollViewContent: {
+ flex: 1
+ },
+ text: {
+ fontSize: 15,
+ color: theme.centerChannelColor
+ },
+ top: {
+ padding: 25,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ username: {
+ marginTop: 15,
+ color: theme.centerChannelColor,
+ fontSize: 15
+ }
+ });
+});
+
+export default class UserProfile extends PureComponent {
+ static propTypes = {
+ currentUserId: PropTypes.string.isRequired,
+ theme: PropTypes.object.isRequired,
+ user: PropTypes.object.isRequired,
+ actions: PropTypes.shape({
+ handleSendMessage: PropTypes.func.isRequired
+ }).isRequired
+ };
+
+ state = {
+ isFavorite: false
+ }
+
+ getDisplayName = () => {
+ const {theme, user} = this.props;
+ const style = createStyleSheet(theme);
+
+ const displayName = getFullName(user);
+
+ if (displayName) {
+ return {displayName};
+ }
+
+ return null;
+ }
+
+ buildDisplayBlock = (property) => {
+ const {theme, user} = this.props;
+ const style = createStyleSheet(theme);
+
+ if (user.hasOwnProperty(property) && user[property].length > 0) {
+ return (
+
+ {property.toUpperCase()}
+ {user[property]}
+
+ );
+ }
+
+ return null;
+ }
+
+ sendMessage = () => {
+ this.props.actions.handleSendMessage(this.props.user.id);
+ }
+
+ render() {
+ const {theme, user} = this.props;
+ const style = createStyleSheet(theme);
+
+ return (
+
+
+
+
+ {this.getDisplayName()}
+ {`@${user.username}`}
+
+
+ {this.buildDisplayBlock('username')}
+ {this.buildDisplayBlock('email')}
+ {this.buildDisplayBlock('position')}
+
+ {this.props.currentUserId !== this.props.user.id &&
+
+ }
+
+
+ );
+ }
+}
diff --git a/app/scenes/user_profile/user_profile_container.js b/app/scenes/user_profile/user_profile_container.js
new file mode 100644
index 000000000..52b0a799c
--- /dev/null
+++ b/app/scenes/user_profile/user_profile_container.js
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import {bindActionCreators} from 'redux';
+
+import {handleSendMessage} from 'app/actions/views/user_profile';
+import {getTheme} from 'service/selectors/entities/preferences';
+
+import UserProfile from './user_profile';
+
+import navigationSceneConnect from '../navigationSceneConnect';
+
+function mapStateToProps(state, ownProps) {
+ return {
+ currentUserId: state.entities.users.currentId,
+ user: state.entities.users.profiles[ownProps.userId],
+ theme: getTheme(state)
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ handleSendMessage
+ }, dispatch)
+ };
+}
+
+export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(UserProfile);
diff --git a/app/scenes/user_profile/user_profile_row.js b/app/scenes/user_profile/user_profile_row.js
new file mode 100644
index 000000000..af12b4b2d
--- /dev/null
+++ b/app/scenes/user_profile/user_profile_row.js
@@ -0,0 +1,130 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React, {PropTypes} from 'react';
+import {
+ StyleSheet,
+ Switch,
+ Text,
+ TouchableHighlight,
+ View
+} from 'react-native';
+import Icon from 'react-native-vector-icons/FontAwesome';
+
+import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
+import FormattedText from 'app/components/formatted_text';
+
+const createStyleSheet = makeStyleSheetFromTheme((theme) => {
+ return StyleSheet.create({
+ container: {
+ backgroundColor: changeOpacity(theme.centerChannelBg, 0.7),
+ paddingHorizontal: 15,
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderTopWidth: 1,
+ borderBottomWidth: 1,
+ borderTopColor: changeOpacity(theme.centerChannelColor, 0.3),
+ borderBottomColor: changeOpacity(theme.centerChannelColor, 0.3)
+ },
+ detail: {
+ marginHorizontal: 15,
+ color: 'rgba(0, 0, 0, 0.5)',
+ fontSize: 15
+ },
+ label: {
+ flex: 1,
+ marginLeft: 15,
+ fontSize: 15,
+ paddingVertical: 15,
+ color: theme.centerChannelColor
+ },
+ leftIcon: {
+ width: 17,
+ color: theme.centerChannelColor
+ },
+ rightIcon: {
+ color: theme.centerChannelColor,
+ opacity: 0.7
+ },
+ wrapper: {
+ backgroundColor: '#ddd'
+ }
+ });
+});
+
+function createTouchableComponent(children, action) {
+ return (
+
+ {children}
+
+ );
+}
+
+function userProfileRow(props) {
+ const {action, defaultMessage, detail, icon, textId, togglable, theme, shouldRender = true} = props;
+
+ if (!shouldRender) {
+ return null;
+ }
+
+ const style = createStyleSheet(theme);
+
+ const RowComponent = (
+
+
+
+
+ {detail}
+ {togglable ?
+ :
+
+ }
+
+
+ );
+
+ if (togglable) {
+ return RowComponent;
+ }
+
+ return createTouchableComponent(RowComponent, action);
+}
+
+userProfileRow.propTypes = {
+ action: PropTypes.func.isRequired,
+ defaultMessage: PropTypes.string.isRequired,
+ detail: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.bool
+ ]),
+ icon: PropTypes.string.isRequired,
+ iconColor: PropTypes.string,
+ textId: PropTypes.string.isRequired,
+ togglable: PropTypes.bool,
+ textColor: PropTypes.string,
+ theme: PropTypes.object.isRequired
+};
+
+userProfileRow.defaultProps = {
+ iconColor: 'rgba(0, 0, 0, 0.7)',
+ textColor: '#000',
+ togglable: false
+};
+
+export default userProfileRow;
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 2f6d8cbf6..e4ce87234 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -1519,6 +1519,8 @@
"mobile.routes.channel_members.action_message": "You must select at least one member to {term} {prep} the channel.",
"mobile.routes.channel_members.action_message_confirm": "Are you sure you want to {term} the selected members {prep} the channel?",
"mobile.routes.thread": "Thread",
+ "mobile.routes.user_profile": "Profile",
+ "mobile.routes.user_profile.send_message": "Send Message",
"more_channels.close": "Close",
"more_channels.create": "Create New Channel",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",
diff --git a/test/app/reducers/navigation.test.js b/test/app/reducers/navigation.test.js
index 31fa8c5c3..e84d445eb 100644
--- a/test/app/reducers/navigation.test.js
+++ b/test/app/reducers/navigation.test.js
@@ -154,6 +154,33 @@ describe('Reducers.Navigation', () => {
});
});
+ it('NAVIGATION_POP_TO_INDEX', () => {
+ let state = initialState();
+
+ state = reduceAndFreeze(state, {
+ type: NavigationTypes.NAVIGATION_PUSH,
+ route: {key: 'fake'}
+ });
+ state = reduceAndFreeze(state, {
+ type: NavigationTypes.NAVIGATION_PUSH,
+ route: {key: 'fake2'}
+ });
+ state = reduceAndFreeze(state, {
+ type: NavigationTypes.NAVIGATION_PUSH,
+ route: {key: 'fake3'}
+ });
+
+ it('Reset to first index', () => {
+ state = reduceAndFreeze(state, {
+ type: NavigationTypes.NAVIGATION_POP_TO_INDEX,
+ index: 0
+ });
+
+ assert.equal(state.index, 0);
+ assert.deepEqual(state.routes, [Routes.Root, {key: 'fake'}]);
+ });
+ });
+
it('NAVIGATION_OPEN_LEFT_DRAWER', () => {
let state = initialState();