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:
parent
322e63d7c4
commit
f5d77fcebc
12 changed files with 1914 additions and 278 deletions
423
app/components/edit_channel_info/index.js
Normal file
423
app/components/edit_channel_info/index.js
Normal 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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
300
app/screens/edit_channel/edit_channel.js
Normal file
300
app/screens/edit_channel/edit_channel.js
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
39
app/screens/edit_channel/index.js
Normal file
39
app/screens/edit_channel/index.js
Normal 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);
|
||||
|
|
@ -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
1003
app/utils/latinise.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue