Add Channel Info Edit options to ChannelInfo screen (#1189)

* Add Channel Info Edit options to ChannelInfo screen

Refactor CreateChannel to CreateChannel and ChannelInfo

Add EditChannel screen

add updateChannelRequest

display header option only if channel is DM or GM

Add editing prop to ChannelInfo

Add saving state to ChannelInfo

Clean up EditChannel and change saving to updating

Add channel name (url) field in EditChannel

Clean up enableRightButton logic for EditChannel

Add error handling for EditChannel fields

Remove leftButton declaration from EditChannel

Add wrapWithPreventDoubleTap to click handlers in ChannelInfo

Move channel info to parent component (CreateChannel)

Add editing support for channel info in parent component

Remove ownProps from connected components

Remove passing parent props using the spread operator

* Strip out channel url field from EditChannel

* Comment out channelUrl textInput ref

Make eslint happy

* Remove injectIntl in favor of intl context props

* Add mobile prefix to json string

* Remove unnecessary actions declaration in ChannelInfo

* Refactor creating | editing to saving

* Safe check refs when blurring in ChannelInfo

* pass channelType for displayingHeaderOnly in EditChannelInfo

* Refactor ChannelInfo component to EditChannelInfo

* pass 1to1 props instead of object

* Add required to certain props in EditChannelInfo

* Use Stylesheet for container styles

* Add InvalidPermission method when updating channel

* Clean up onDisplayNameChangeText and safe check scrollToEnd

* Add {deviceWidth, deviceHeight} from redux

* Remove style.navTitle

* Add wrapper methods for delete and leave channel

* remove alertErrorIfInvalidPermissions

* Fix mobile localization strings to use prefix mobile.-

* Add TODO for channel URL
This commit is contained in:
Chris Duarte 2017-11-28 14:48:51 -08:00 committed by enahum
parent 322e63d7c4
commit f5d77fcebc
12 changed files with 1914 additions and 278 deletions

View file

@ -0,0 +1,423 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableWithoutFeedback,
View,
Text,
findNodeHandle
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
import {getShortenedURL} from 'app/utils/url';
export default class EditChannelInfo extends PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
channelType: PropTypes.string,
enableRightButton: PropTypes.func,
saving: PropTypes.bool.isRequired,
editing: PropTypes.bool,
error: PropTypes.string,
displayName: PropTypes.string,
currentTeamUrl: PropTypes.string,
channelURL: PropTypes.string,
purpose: PropTypes.string,
header: PropTypes.string,
onDisplayNameChange: PropTypes.func,
onChannelURLChange: PropTypes.func,
onPurposeChange: PropTypes.func,
onHeaderChange: PropTypes.func,
oldDisplayName: PropTypes.string,
oldChannelURL: PropTypes.string,
oldHeader: PropTypes.string,
oldPurpose: PropTypes.string
};
static defaultProps = {
editing: false
};
blur = () => {
if (this.nameInput) {
this.nameInput.refs.wrappedInstance.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput) {
// this.urlInput.refs.wrappedInstance.blur();
// }
if (this.purposeInput) {
this.purposeInput.refs.wrappedInstance.blur();
}
if (this.headerInput) {
this.headerInput.refs.wrappedInstance.blur();
}
if (this.scroll) {
this.scroll.scrollToPosition(0, 0, true);
}
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelURLRef = (ref) => {
this.urlInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
EventEmitter.emit('closing-create-channel', false);
if (goBack) {
this.props.navigator.pop({animated: true});
} else {
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
}
};
lastTextRef = (ref) => {
this.lastText = ref;
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
oldChannelURL,
oldPurpose,
oldHeader
} = this.props;
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
purpose !== oldPurpose || header !== oldHeader;
};
enableRightButton = (enable = false) => {
this.props.enableRightButton(enable);
};
onDisplayNameChangeText = (displayName) => {
const {editing, onDisplayNameChange} = this.props;
onDisplayNameChange(displayName);
if (editing) {
const {channelURL, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
return;
}
const displayNameExists = displayName && displayName.length >= 2;
this.props.enableRightButton(displayNameExists);
};
onDisplayURLChangeText = (channelURL) => {
const {editing, onChannelURLChange} = this.props;
onChannelURLChange(channelURL);
if (editing) {
const {displayName, purpose, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onPurposeChangeText = (purpose) => {
const {editing, onPurposeChange} = this.props;
onPurposeChange(purpose);
if (editing) {
const {displayName, channelURL, header} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
onHeaderChangeText = (header) => {
const {editing, onHeaderChange} = this.props;
onHeaderChange(header);
if (editing) {
const {displayName, channelURL, purpose} = this.props;
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
this.enableRightButton(canUpdate);
}
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
if (this.scroll && this.lastText) {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
}
};
render() {
const {
theme,
editing,
channelType,
currentTeamUrl,
deviceWidth,
deviceHeight,
displayName,
channelURL,
header,
purpose
} = this.props;
const {error, saving} = this.props;
const fullUrl = currentTeamUrl + '/channels';
const shortUrl = getShortenedURL(fullUrl, 35);
const style = getStyleSheet(theme);
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
channelType === General.GM_CHANNEL;
if (saving) {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {deviceWidth}]}>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</View>
);
}
return (
<View style={style.container}>
<StatusBar/>
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: deviceHeight + (Platform.OS === 'android' ? 200 : 0)}]}>
{!displayHeaderOnly && (
<View>
<View>
<FormattedText
style={style.title}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
</View>
)}
{/*TODO: Hide channel url field until it's added to CreateChannel */}
{false && editing && !displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='rename_channel.url'
defaultMessage='URL'
/>
<Text style={style.optional}>
{shortUrl}
</Text>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelURLRef}
value={channelURL}
onChangeText={this.onDisplayURLChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'rename_channel.handleHolder', defaultMessage: 'lowercase alphanumeric characters'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
</View>
)}
{!displayHeaderOnly && (
<View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
textAlignVertical='top'
underlineColorAndroid='transparent'
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
</View>
)}
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.scrollToEnd}
textAlignVertical='top'
underlineColorAndroid='transparent'
/>
</View>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
paddingTop: 10
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center'
},
inputContainer: {
marginTop: 10,
backgroundColor: '#fff'
},
input: {
color: '#333',
fontSize: 14,
height: 40,
paddingHorizontal: 15
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15
}
};
});

View file

@ -65,5 +65,7 @@ export default {
...ViewTypes,
POST_VISIBILITY_CHUNK_SIZE: 15,
FEATURE_TOGGLE_PREFIX: 'feature_enabled_',
EMBED_PREVIEW: 'embed_preview'
EMBED_PREVIEW: 'embed_preview',
MIN_CHANNELNAME_LENGTH: 2,
MAX_CHANNELNAME_LENGTH: 22
};

View file

@ -12,7 +12,7 @@ import {
} from 'react-native';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {alertErrorWithFallback} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -35,6 +35,7 @@ class ChannelInfo extends PureComponent {
isCurrent: PropTypes.bool.isRequired,
isFavorite: PropTypes.bool.isRequired,
canManageUsers: PropTypes.bool.isRequired,
canEditChannel: PropTypes.bool.isRequired,
actions: PropTypes.shape({
closeDMChannel: PropTypes.func.isRequired,
closeGMChannel: PropTypes.func.isRequired,
@ -74,7 +75,7 @@ class ChannelInfo extends PureComponent {
}
};
goToChannelAddMembers = () => {
goToChannelAddMembers = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
@ -88,9 +89,9 @@ class ChannelInfo extends PureComponent {
screenBackgroundColor: theme.centerChannelBg
}
});
};
});
goToChannelMembers = () => {
goToChannelMembers = wrapWithPreventDoubleTap(() => {
const {canManageUsers, intl, navigator, theme} = this.props;
const id = canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers';
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
@ -107,9 +108,36 @@ class ChannelInfo extends PureComponent {
screenBackgroundColor: theme.centerChannelBg
}
});
});
handleChannelEdit = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const id = 'mobile.channel_info.edit';
const defaultMessage = 'Edit Channel';
navigator.push({
backButtonTitle: '',
screen: 'EditChannel',
title: intl.formatMessage({id, defaultMessage}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
});
handleLeave = () => {
this.handleDeleteOrLeave('leave');
};
handleDeleteOrLeave(eventType) {
handleDelete = () => {
this.handleDeleteOrLeave('delete');
};
handleDeleteOrLeave = wrapWithPreventDoubleTap((eventType) => {
const {formatMessage} = this.props.intl;
const channel = this.props.currentChannel;
const term = channel.type === General.OPEN_CHANNEL ?
@ -171,9 +199,9 @@ class ChannelInfo extends PureComponent {
onPress: onPressAction
}],
);
}
});
handleClose = () => {
handleClose = wrapWithPreventDoubleTap(() => {
const {currentChannel, isCurrent, isFavorite} = this.props;
const channel = Object.assign({}, currentChannel, {isCurrent}, {isFavorite});
const {closeDMChannel, closeGMChannel} = this.props.actions;
@ -190,7 +218,7 @@ class ChannelInfo extends PureComponent {
});
break;
}
};
});
handleFavorite = () => {
const {isFavorite, actions, currentChannel} = this.props;
@ -232,6 +260,7 @@ class ChannelInfo extends PureComponent {
currentChannelCreatorName,
currentChannelMemberCount,
canManageUsers,
canEditChannel,
navigator,
status,
theme
@ -299,7 +328,7 @@ class ChannelInfo extends PureComponent {
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={() => preventDoubleTap(this.goToChannelMembers)}
action={this.goToChannelMembers}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
@ -312,7 +341,7 @@ class ChannelInfo extends PureComponent {
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={() => preventDoubleTap(this.goToChannelAddMembers)}
action={this.goToChannelAddMembers}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'
@ -320,11 +349,23 @@ class ChannelInfo extends PureComponent {
/>
</View>
}
{canEditChannel && (
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={this.handleChannelEdit}
defaultMessage='Edit Channel'
icon='edit'
textId='mobile.channel_info.edit'
theme={theme}
/>
</View>
)}
{this.renderLeaveOrDeleteChannelRow() &&
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={() => preventDoubleTap(this.handleDeleteOrLeave, this, 'leave')}
action={this.handleLeave}
defaultMessage='Leave Channel'
icon='sign-out'
textId='navbar.leave'
@ -336,7 +377,7 @@ class ChannelInfo extends PureComponent {
{this.renderLeaveOrDeleteChannelRow() && canDeleteChannel &&
<View style={style.footer}>
<ChannelInfoRow
action={() => preventDoubleTap(this.handleDeleteOrLeave, this, 'delete')}
action={this.handleDelete}
defaultMessage='Delete Channel'
icon='trash'
iconColor='#CA3B27'
@ -349,7 +390,7 @@ class ChannelInfo extends PureComponent {
{this.renderCloseDirect() &&
<View style={style.footer}>
<ChannelInfoRow
action={() => preventDoubleTap(this.handleClose, this)}
action={this.handleClose}
defaultMessage={defaultMessage}
icon='times'
iconColor='#CA3B27'

View file

@ -20,7 +20,7 @@ import {
canManageChannelMembers
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getUserIdFromChannelName, showDeleteOption} from 'mattermost-redux/utils/channel_utils';
import {getUserIdFromChannelName, showDeleteOption, showManagementOptions} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isChannelAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import ChannelInfo from './channel_info';
@ -47,6 +47,7 @@ function mapStateToProps(state) {
return {
canDeleteChannel: showDeleteOption(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)),
canEditChannel: showManagementOptions(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)),
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,

View file

@ -1,36 +1,25 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {intlShape} from 'react-intl';
import {
Dimensions,
Keyboard,
InteractionManager,
Platform,
TouchableWithoutFeedback,
View,
findNodeHandle
InteractionManager
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import EditChannelInfo from 'app/components/edit_channel_info';
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class CreateChannel extends PureComponent {
export default class CreateChannel extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
createChannelRequest: PropTypes.object.isRequired,
navigator: PropTypes.object,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
createChannelRequest: PropTypes.object.isRequired,
channelType: PropTypes.string,
closeButton: PropTypes.object,
actions: PropTypes.shape({
@ -38,6 +27,10 @@ class CreateChannel extends PureComponent {
})
};
static contextTypes = {
intl: intlShape
};
static defaultProps = {
channelType: General.OPEN_CHANNEL
};
@ -52,15 +45,18 @@ class CreateChannel extends PureComponent {
showAsAction: 'always'
};
constructor(props) {
constructor(props, context) {
super(props);
this.state = {
error: null,
creating: false,
displayName: '',
header: '',
purpose: ''
purpose: '',
header: ''
};
this.rightButton.title = props.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
this.rightButton.title = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
if (props.channelType === General.PRIVATE_CHANNEL) {
this.left = {...this.leftButton, icon: props.closeButton};
@ -107,25 +103,6 @@ class CreateChannel extends PureComponent {
}
}
blur = () => {
this.nameInput.refs.wrappedInstance.blur();
this.purposeInput.refs.wrappedInstance.blur();
this.headerInput.refs.wrappedInstance.blur();
this.scroll.scrollToPosition(0, 0, true);
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
EventEmitter.emit('closing-create-channel', false);
if (goBack) {
@ -161,25 +138,12 @@ class CreateChannel extends PureComponent {
this.props.navigator.setButtons(buttons);
};
lastTextRef = (ref) => {
this.lastText = ref;
};
onCreateChannel = () => {
Keyboard.dismiss();
const {displayName, purpose, header} = this.state;
this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType);
};
onDisplayNameChangeText = (displayName) => {
this.setState({displayName});
if (displayName && displayName.length >= 2) {
this.emitCanCreateChannel(true);
} else {
this.emitCanCreateChannel(false);
}
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
@ -193,220 +157,50 @@ class CreateChannel extends PureComponent {
}
};
onPurposeChangeText = (purpose) => {
onDisplayNameChange = (displayName) => {
this.setState({displayName});
};
onPurposeChange = (purpose) => {
this.setState({purpose});
};
onHeaderChangeText = (header) => {
onHeaderChange = (header) => {
this.setState({header});
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
};
render() {
const {theme} = this.props;
const {creating, displayName, header, purpose, error} = this.state;
const {height, width} = Dimensions.get('window');
const style = getStyleSheet(theme);
if (creating) {
return (
<View style={{flex: 1}}>
<StatusBar/>
<Loading/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {width}]}>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</View>
);
}
const {
navigator,
theme,
deviceWidth,
deviceHeight
} = this.props;
const {
error,
creating,
displayName,
purpose,
header
} = this.state;
return (
<View style={{flex: 1}}>
<StatusBar/>
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
>
{displayError}
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: height + (Platform.OS === 'android' ? 200 : 0)}]}>
<View>
<FormattedText
style={style.title}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
underlineColorAndroid='transparent'
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
blurOnSubmit={false}
onFocus={this.scrollToEnd}
underlineColorAndroid='transparent'
/>
</View>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
</View>
<EditChannelInfo
navigator={navigator}
theme={theme}
enableRightButton={this.emitCanCreateChannel}
error={error}
saving={creating}
onDisplayNameChange={this.onDisplayNameChange}
onPurposeChange={this.onPurposeChange}
onHeaderChange={this.onHeaderChange}
displayName={displayName}
purpose={purpose}
header={header}
deviceWidth={deviceWidth}
deviceHeight={deviceHeight}
/>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
paddingTop: 10
},
errorContainer: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center'
},
inputContainer: {
marginTop: 10,
backgroundColor: '#fff'
},
input: {
color: '#333',
fontSize: 14,
height: 40,
paddingHorizontal: 15
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15
},
navTitle: {
...Platform.select({
android: {
fontSize: 18
},
ios: {
fontSize: 15,
fontWeight: 'bold'
}
})
}
};
});
export default injectIntl(CreateChannel);

View file

@ -7,16 +7,19 @@ import {connect} from 'react-redux';
import {handleCreateChannel} from 'app/actions/views/create_channel';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import CreateChannel from './create_channel';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const {createChannel: createChannelRequest} = state.requests.channels;
const {deviceWidth, deviceHeight} = getDimensions(state);
return {
createChannelRequest,
channelType: ownProps.channelType,
theme: getTheme(state)
theme: getTheme(state),
deviceWidth,
deviceHeight
};
}

View file

@ -0,0 +1,300 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Keyboard,
InteractionManager
} from 'react-native';
import EditChannelInfo from 'app/components/edit_channel_info';
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {ViewTypes} from 'app/constants';
import {cleanUpUrlable} from 'app/utils/url';
const messages = {
display_name_required: {
id: 'mobile.rename_channel.display_name_required',
defaultMessage: 'Channel name is required'
},
display_name_maxLength: {
id: 'mobile.rename_channel.display_name_maxLength',
defaultMessage: 'Channel name must be less than {maxLength, number} characters'
},
display_name_minLength: {
id: 'mobile.rename_channel.display_name_minLength',
defaultMessage: 'Channel name must be {minLength, number} or more characters'
},
name_required: {
id: 'mobile.rename_channel.name_required',
defaultMessage: 'URL is required'
},
name_maxLength: {
id: 'mobile.rename_channel.name_maxLength',
defaultMessage: 'URL must be less than {maxLength, number} characters'
},
name_minLength: {
id: 'mobile.rename_channel.name_minLength',
defaultMessage: 'URL must be {minLength, number} or more characters'
},
name_lowercase: {
id: 'mobile.rename_channel.name_lowercase',
defaultMessage: 'URL be lowercase alphanumeric characters'
}
};
export default class EditChannel extends PureComponent {
static propTypes = {
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
deviceWidth: PropTypes.number.isRequired,
deviceHeight: PropTypes.number.isRequired,
channel: PropTypes.object.isRequired,
currentTeamUrl: PropTypes.string.isRequired,
updateChannelRequest: PropTypes.object.isRequired,
closeButton: PropTypes.object,
actions: PropTypes.shape({
patchChannel: PropTypes.func.isRequired
})
};
static contextTypes = {
intl: intlShape
};
rightButton = {
id: 'edit-channel',
disabled: true,
showAsAction: 'always'
};
constructor(props, context) {
super(props);
const {
channel: {
display_name: displayName,
header,
purpose,
name: channelURL
}
} = props;
this.state = {
error: null,
updating: false,
displayName,
channelURL,
purpose,
header
};
this.rightButton.title = context.intl.formatMessage({id: 'mobile.edit_channel', defaultMessage: 'Save'});
const buttons = {
rightButtons: [this.rightButton]
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons(buttons);
}
componentDidMount() {
this.emitCanUpdateChannel(false);
}
componentWillReceiveProps(nextProps) {
const {updateChannelRequest} = nextProps;
if (this.props.updateChannelRequest !== updateChannelRequest) {
switch (updateChannelRequest.status) {
case RequestStatus.STARTED:
this.emitUpdating(true);
this.setState({error: null, updating: true});
break;
case RequestStatus.SUCCESS:
EventEmitter.emit('close_channel_drawer');
InteractionManager.runAfterInteractions(() => {
this.emitUpdating(false);
this.setState({error: null, updating: false});
this.close();
});
break;
case RequestStatus.FAILURE:
this.emitUpdating(false);
this.setState({error: updateChannelRequest.error, updating: false});
break;
}
}
}
close = () => {
this.props.navigator.pop({animated: true});
};
emitCanUpdateChannel = (enabled) => {
const buttons = {
rightButtons: [{...this.rightButton, disabled: !enabled}]
};
this.props.navigator.setButtons(buttons);
};
emitUpdating = (loading) => {
const buttons = {
rightButtons: [{...this.rightButton, disabled: loading}]
};
this.props.navigator.setButtons(buttons);
};
validateDisplayName = (displayName) => {
const {formatMessage} = this.context.intl;
if (!displayName) {
return {error: formatMessage(messages.display_name_required)};
} else if (displayName.length > ViewTypes.MAX_CHANNELNAME_LENGTH) {
return {error: formatMessage(
messages.display_name_maxLength,
{maxLength: ViewTypes.MAX_CHANNELNAME_LENGTH}
)};
} else if (displayName.length < ViewTypes.MIN_CHANNELNAME_LENGTH) {
return {error: formatMessage(
messages.display_name_minLength,
{minLength: ViewTypes.MIN_CHANNELNAME_LENGTH}
)};
}
return {error: null};
};
validateChannelURL = (channelURL) => {
const {formatMessage} = this.context.intl;
if (!channelURL) {
return {error: formatMessage(messages.name_required)};
} else if (channelURL.length > ViewTypes.MAX_CHANNELNAME_LENGTH) {
return {error: formatMessage(
messages.name_maxLength,
{maxLength: ViewTypes.MAX_CHANNELNAME_LENGTH}
)};
}
const cleanedName = cleanUpUrlable(channelURL);
if (cleanedName === channelURL) {
return {error: null};
}
return {error: formatMessage(messages.name_lowercase)};
};
onUpdateChannel = () => {
Keyboard.dismiss();
const {displayName, channelURL, purpose, header} = this.state;
const {channel: {id}} = this.props;
const channel = {
display_name: displayName,
name: channelURL,
purpose,
header
};
let result = this.validateDisplayName(displayName.trim());
if (result.error) {
this.setState({error: result.error});
return;
}
result = this.validateChannelURL(channelURL.trim());
if (result.error) {
this.setState({error: result.error});
return;
}
this.props.actions.patchChannel(id, channel);
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'close-edit-channel':
this.close();
break;
case 'edit-channel':
this.onUpdateChannel();
break;
}
}
};
onDisplayNameChange = (displayName) => {
this.setState({displayName});
};
onChannelURLChange = (channelURL) => {
this.setState({channelURL});
};
onPurposeChange = (purpose) => {
this.setState({purpose});
};
onHeaderChange = (header) => {
this.setState({header});
};
render() {
const {
channel: {
display_name: oldDisplayName,
name: oldChannelURL,
header: oldHeader,
purpose: oldPurpose,
type
},
navigator,
theme,
currentTeamUrl,
deviceWidth,
deviceHeight
} = this.props;
const {
error,
updating,
displayName,
channelURL,
purpose,
header
} = this.state;
return (
<EditChannelInfo
navigator={navigator}
theme={theme}
enableRightButton={this.emitCanUpdateChannel}
error={error}
saving={updating}
channelType={type}
currentTeamUrl={currentTeamUrl}
onDisplayNameChange={this.onDisplayNameChange}
onChannelURLChange={this.onChannelURLChange}
onPurposeChange={this.onPurposeChange}
onHeaderChange={this.onHeaderChange}
displayName={displayName}
channelURL={channelURL}
header={header}
purpose={purpose}
editing={true}
oldDisplayName={oldDisplayName}
oldChannelURL={oldChannelURL}
oldPurpose={oldPurpose}
oldHeader={oldHeader}
deviceWidth={deviceWidth}
deviceHeight={deviceHeight}
/>
);
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamUrl} from 'mattermost-redux/selectors/entities/teams';
import {patchChannel} from 'mattermost-redux/actions/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import EditChannel from './edit_channel';
function mapStateToProps(state) {
const {updateChannel: updateChannelRequest} = state.requests.channels;
const channel = getCurrentChannel(state);
const {deviceWidth, deviceHeight} = getDimensions(state);
return {
channel,
currentTeamUrl: getCurrentTeamUrl(state),
updateChannelRequest,
theme: getTheme(state),
deviceWidth,
deviceHeight
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
patchChannel
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EditChannel);

View file

@ -11,6 +11,7 @@ import AdvancedSettings from 'app/screens/settings/advanced_settings';
import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
import EditChannel from 'app/screens/edit_channel';
import ChannelMembers from 'app/screens/channel_members';
import ChannelPeek from 'app/screens/channel_peek';
import ClientUpgrade from 'app/screens/client_upgrade';
@ -63,6 +64,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel, false), store, Provider);
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(EditChannel), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
Navigation.registerComponent('ChannelPeek', () => wrapWithContextProvider(ChannelPeek), store, Provider);
Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(ClientUpgrade), store, Provider);

1003
app/utils/latinise.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {latinise} from './latinise.js';
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
const imgRegex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i;
@ -56,3 +58,20 @@ export function normalizeProtocol(url) {
const protocol = url.substring(0, index);
return protocol.toLowerCase() + url.substring(index);
}
export function getShortenedURL(url = '', getLength = 27) {
if (url.length > 35) {
const subLength = getLength - 14;
return url.substring(0, 10) + '...' + url.substring(url.length - subLength, url.length) + '/';
}
return url + '/';
}
export function cleanUpUrlable(input) {
var cleaned = latinise(input);
cleaned = cleaned.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
cleaned = cleaned.replace(/-{2,}/, '-');
cleaned = cleaned.replace(/^-+/, '');
cleaned = cleaned.replace(/-+$/, '');
return cleaned;
}

View file

@ -1918,6 +1918,7 @@
"mobile.channel_info.delete_failed": "We couldn't delete the channel {displayName}. Please check your connection and try again.",
"mobile.channel_info.privateChannel": "Private Channel",
"mobile.channel_info.publicChannel": "Public Channel",
"mobile.channel_info.edit": "Edit Channel",
"mobile.channel_list.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?",
"mobile.channel_list.alertNo": "No",
"mobile.channel_list.alertTitleLeaveChannel": "Leave {term}",
@ -1954,6 +1955,7 @@
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
"mobile.components.select_server_view.proceed": "Proceed",
"mobile.components.select_server_view.siteUrlPlaceholder": "https://mattermost.example.com",
"mobile.edit_channel": "Save",
"mobile.create_channel": "Create",
"mobile.create_channel.private": "New Private Channel",
"mobile.create_channel.public": "New Public Channel",
@ -2061,6 +2063,13 @@
"mobile.post.retry": "Refresh",
"mobile.post_info.add_reaction": "Add Reaction",
"mobile.post_info.copy_post": "Copy Post",
"mobile.rename_channel.display_name_required": "Channel name is required",
"mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters",
"mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters",
"mobile.rename_channel.name_required": "URL is required",
"mobile.rename_channel.name_maxLength": "URL must be less than {maxLength, number} characters",
"mobile.rename_channel.name_minLength": "URL must be {minLength, number} or more characters",
"mobile.rename_channel.name_lowercase": "URL be lowercase alphanumeric characters",
"mobile.request.invalid_response": "Received invalid response from the server.",
"mobile.retry_message": "Refreshing messages failed. Pull up to try again.",
"mobile.routes.channelInfo": "Info",