Add channel-info (#164)

Adding channel info screen.
This commit is contained in:
Chris Duarte 2017-01-20 12:17:28 -08:00 committed by Harrison Healey
parent b8dd3ec359
commit e0eb79656e
16 changed files with 499 additions and 8 deletions

View file

@ -66,3 +66,12 @@ export function goToFlaggedPosts() {
}, getState);
};
}
export function goToChannelInfo() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.ChannelInfo
}, getState);
};
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {Text} from 'react-native';
class FormattedDate extends Component {
static propTypes = {
intl: intlShape.isRequired,
value: PropTypes.any.isRequired,
format: PropTypes.string,
children: PropTypes.func
};
render() {
const {
intl,
value,
children,
...props
} = this.props;
Reflect.deleteProperty(props, 'format');
const formattedDate = intl.formatDate(value, this.props);
if (typeof children === 'function') {
return children(formattedDate);
}
return <Text>{formattedDate}</Text>;
}
}
export default injectIntl(FormattedDate);

View file

@ -22,5 +22,9 @@ export default {
},
Search: {
key: 'Search'
},
ChannelInfo: {
key: 'ChannelInfo',
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}
}
};

View file

@ -23,7 +23,8 @@ export default class Channel extends React.PureComponent {
loadProfilesAndTeamMembersForDMSidebar: React.PropTypes.func.isRequired,
selectInitialChannel: React.PropTypes.func.isRequired,
openChannelDrawer: React.PropTypes.func.isRequired,
handlePostDraftChanged: React.PropTypes.func.isRequired
handlePostDraftChanged: React.PropTypes.func.isRequired,
goToChannelInfo: React.PropTypes.func.isRequired
}).isRequired,
currentTeam: React.PropTypes.object,
currentChannel: React.PropTypes.object,
@ -110,6 +111,7 @@ export default class Channel extends React.PureComponent {
currentChannel={currentChannel}
openLeftDrawer={this.openChannelDrawer}
openRightDrawer={this.openRightSidebar}
goToChannelInfo={this.props.actions.goToChannelInfo}
/>
<ChannelPostList channel={currentChannel}/>
<PostTextbox

View file

@ -15,6 +15,7 @@ import {openChannelDrawer} from 'app/actions/views/drawer';
import {getCurrentChannel} from 'service/selectors/entities/channels';
import {getTheme} from 'service/selectors/entities/preferences';
import {getCurrentTeam} from 'service/selectors/entities/teams';
import {goToChannelInfo} from 'app/actions/navigation';
import Channel from './channel';
@ -35,7 +36,8 @@ function mapDispatchToProps(dispatch) {
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel,
openChannelDrawer,
handlePostDraftChanged
handlePostDraftChanged,
goToChannelInfo
}, dispatch)
};
}

View file

@ -14,7 +14,8 @@ export default class ChannelHeader extends React.PureComponent {
displayName: React.PropTypes.string.isRequired,
theme: React.PropTypes.object.isRequired,
openLeftDrawer: React.PropTypes.func.isRequired,
openRightDrawer: React.PropTypes.func.isRequired
openRightDrawer: React.PropTypes.func.isRequired,
goToChannelInfo: React.PropTypes.func.isRequired
}
render() {
@ -38,9 +39,13 @@ export default class ChannelHeader extends React.PureComponent {
</TouchableHighlight>
</View>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-start', alignItems: 'center', backgroundColor: theme.sidebarHeaderBg}}>
<Text style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
{displayName}
</Text>
<TouchableHighlight onPress={this.props.goToChannelInfo}>
<View>
<Text style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}>
{displayName}
</Text>
</View>
</TouchableHighlight>
</View>
<TouchableHighlight
onPress={this.props.openRightDrawer}

View file

@ -0,0 +1,132 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {
ScrollView,
StyleSheet,
View
} from 'react-native';
import ChannelInfoHeader from './channel_info_header';
import ChannelInfoRow from './channel_info_row';
const style = StyleSheet.create({
container: {
flex: 1
},
footer: {
marginTop: 40
},
separator: {
flex: 1,
marginHorizontal: 15
},
separatorContainer: {
flex: 1,
backgroundColor: '#fff',
height: 1
},
scrollView: {
flex: 1
}
});
export default class ChannelInfo extends PureComponent {
static propTypes = {
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
isFavorite: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
actions: PropTypes.shape({
getChannelStats: PropTypes.func.isRequired
})
}
componentDidMount() {
this.props.actions.getChannelStats(this.props.currentChannel.team_id, this.props.currentChannel.id);
}
render() {
const {
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
isFavorite,
theme
} = this.props;
return (
<View style={style.container}>
<ScrollView
style={style.scrollView}
contentContainerStyle={{backgroundColor: theme.centerChannelBg}}
>
<ChannelInfoHeader
createAt={currentChannel.create_at}
creator={currentChannelCreatorName}
displayName={currentChannel.display_name}
header={currentChannel.header}
purpose={currentChannel.purpose}
/>
<ChannelInfoRow
action={() => true}
defaultMessage='Favorite'
detail={isFavorite}
icon='star-o'
textId='mobile.routes.channelInfo.favorite'
togglable={true}
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
</View>
<ChannelInfoRow
action={() => true}
defaultMessage='Notification Preferences'
icon='bell-o'
textId='channel_header.notificationPreferences'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
</View>
<ChannelInfoRow
action={() => true}
defaultMessage='Manage Members'
detail={currentChannelMemberCount}
icon='users'
textId='channel_header.manageMembers'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
</View>
<ChannelInfoRow
action={() => true}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'
/>
<View style={style.separatorContainer}>
<View style={[style.separator, {backgroundColor: this.props.theme.centerChannelBg}]}/>
</View>
<ChannelInfoRow
action={() => true}
defaultMessage='Leave Channel'
icon='sign-out'
textId='navbar.leave'
/>
<View style={style.footer}>
<ChannelInfoRow
action={() => true}
defaultMessage='Delete Channel'
icon='trash'
iconColor='#DA4A4A'
textId='mobile.routes.channelInfo.delete_channel'
textColor='#DA4A4A'
/>
</View>
</ScrollView>
</View>
);
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getChannelStats} from 'service/actions/channels';
import {getCurrentChannel, getCurrentChannelStats, getChannelsByCategory} from 'service/selectors/entities/channels';
import {getTheme} from 'service/selectors/entities/preferences';
import {getUser} from 'service/selectors/entities/users';
import ChannelInfo from './channel_info';
function mapStateToProps(state, ownProps) {
const currentChannel = getCurrentChannel(state);
const currentChannelCreator = getUser(state, currentChannel.creator_id);
const currentChannelCreatorName = currentChannelCreator && currentChannelCreator.username;
const currentChannelMemberCount = getCurrentChannelStats(state) && getCurrentChannelStats(state).member_count;
const favoriteChannels = getChannelsByCategory(state).favoriteChannels.map((f) => f.id);
const isFavorite = favoriteChannels.indexOf(currentChannel.id) > -1;
return {
...ownProps,
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,
isFavorite,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getChannelStats
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelInfo);

View file

@ -0,0 +1,116 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {
StyleSheet,
Text,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedDate from 'app/components/formatted_date';
import FormattedText from 'app/components/formatted_text';
const style = StyleSheet.create({
container: {
backgroundColor: '#fff',
marginBottom: 40,
padding: 15
},
channelName: {
marginLeft: 5,
fontSize: 15,
fontWeight: '600'
},
channelNameContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 10
},
createdBy: {
flexDirection: 'row',
fontSize: 11,
opacity: 0.5,
marginTop: 5
},
detail: {
fontSize: 13
},
header: {
fontSize: 12,
opacity: 0.5,
marginBottom: 10
},
section: {
marginTop: 15
},
url: {
fontSize: 11,
opacity: 0.5,
marginBottom: 10
}
});
function channelInfoHeader(props) {
const {createAt, creator, displayName, header, purpose} = props;
return (
<View style={style.container}>
<View style={style.channelNameContainer}>
<Icon
name='globe'
size={15}
color='rgba(0, 0, 0, 0.7)'
/>
<Text style={style.channelName}>{displayName}</Text>
</View>
{purpose.length > 0 &&
<View style={style.section}>
<FormattedText
style={style.header}
id='channel_info.purpose'
defaultMessage='Purpose'
/>
<Text style={style.detail}>{purpose}</Text>
</View>
}
{header.length > 0 &&
<View style={style.section}>
<FormattedText
style={style.header}
id='channel_info.header'
defaultMessage='Header'
/>
<Text style={style.detail}>{header}</Text>
</View>
}
{creator &&
<Text style={style.createdBy}>
<FormattedText
id='mobile.routes.channelInfo.createdBy'
defaultMessage='Created by {creator} on '
values={{
creator
}}
/>
<FormattedDate
value={new Date(createAt)}
year='numeric'
month='long'
day='2-digit'
/>
</Text>
}
</View>
);
}
channelInfoHeader.propTypes = {
createAt: PropTypes.number.isRequired,
creator: PropTypes.string,
displayName: PropTypes.string.isRequired,
header: PropTypes.string,
purpose: PropTypes.string
};
export default channelInfoHeader;

View file

@ -0,0 +1,106 @@
// 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 FormattedText from 'app/components/formatted_text';
const style = StyleSheet.create({
container: {
backgroundColor: '#fff',
paddingHorizontal: 15,
flexDirection: 'row',
alignItems: 'center'
},
detail: {
marginHorizontal: 15,
color: 'rgba(0, 0, 0, 0.5)',
fontSize: 15
},
label: {
flex: 1,
marginLeft: 15,
fontSize: 15,
paddingVertical: 15
},
leftIcon: {
width: 17
}
});
function createTouchableComponent(children, action) {
return (
<TouchableHighlight onPress={action}>
{children}
</TouchableHighlight>
);
}
function channelInfoRow(props) {
const {action, defaultMessage, detail, icon, iconColor, textColor, textId, togglable} = props;
const RowComponent = (
<View style={style.container}>
<Icon
name={icon}
size={15}
color={iconColor}
style={style.leftIcon}
/>
<FormattedText
style={[style.label, {color: textColor}]}
id={textId}
defaultMessage={defaultMessage}
/>
<Text style={style.detail}>{detail}</Text>
{togglable ?
<Switch
action={action}
value={detail}
/> :
<Icon
name='chevron-right'
size={15}
color='rgba(0, 0, 0, 0.7)'
/>
}
</View>
);
if (togglable) {
return RowComponent;
}
return createTouchableComponent(RowComponent, action);
}
channelInfoRow.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
};
channelInfoRow.defaultProps = {
iconColor: 'rgba(0, 0, 0, 0.7)',
textColor: '#000',
togglable: false
};
export default channelInfoRow;

View file

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

View file

@ -7,6 +7,7 @@ import Root from './root/root_container.js';
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 ChannelInfo from './channel_info';
const scenes = {
Root,
@ -14,7 +15,8 @@ const scenes = {
Login,
SelectTeam,
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
Search
Search,
ChannelInfo
};
export function getComponentForScene(key) {

View file

@ -958,7 +958,7 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
"calling_screen": "Calling",
"center_panel.recent": "Click here to jump to recent messages. ",
"chanel_header.addMembers": "Add Members",
"channel_header.addMembers": "Add Members",
"change_url.close": "Close",
"change_url.endWithLetter": "Must end with a letter or number",
"change_url.invalidUrl": "Invalid URL",
@ -1489,6 +1489,10 @@
"mobile.routes.login": "Login",
"mobile.routes.postsList": "Posts List",
"mobile.routes.selectTeam": "Select Team",
"mobile.routes.channelInfo": "Info",
"mobile.routes.channelInfo.createdBy": "Created by {creator} on ",
"mobile.routes.channelInfo.delete_channel": "Delete Channel",
"mobile.routes.channelInfo.favorite": "Favorite",
"more_channels.close": "Close",
"more_channels.create": "Create New Channel",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",

View file

@ -9,6 +9,10 @@ function getAllChannels(state) {
return state.entities.channels.channels;
}
function getAllChannelStats(state) {
return state.entities.channels.stats;
}
export function getCurrentChannelId(state) {
return state.entities.channels.currentId;
}
@ -21,6 +25,14 @@ export const getCurrentChannel = createSelector(
}
);
export const getCurrentChannelStats = createSelector(
getAllChannelStats,
getCurrentChannelId,
(allChannelStats, currentChannelId) => {
return allChannelStats[currentChannelId];
}
);
export const getChannelsOnCurrentTeam = createSelector(
getAllChannels,
getCurrentTeamId,

View file

@ -0,0 +1,6 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export function getCurrentUrl(state) {
return state.entities.general.credentials.url;
}

View file

@ -3,6 +3,8 @@
import {createSelector} from 'reselect';
import {getCurrentUrl} from './general';
export function getCurrentTeamId(state) {
return state.entities.teams.currentId;
}
@ -19,3 +21,10 @@ export const getCurrentTeam = createSelector(
}
);
export const getCurrentTeamUrl = createSelector(
getCurrentUrl,
getCurrentTeam,
(currentUrl, currentTeam) => {
return `${currentUrl}/${currentTeam.name}`;
}
);