PLT-5587 Create channel (#300)
* PLT-5587 Create channel * Feedback review * hidding options in account settings
This commit is contained in:
parent
303574f235
commit
c121097974
23 changed files with 795 additions and 43 deletions
33
NOTICE.txt
33
NOTICE.txt
|
|
@ -84,6 +84,39 @@ SOFTWARE.
|
|||
|
||||
---
|
||||
|
||||
## react-native-keyboard-aware-scroll
|
||||
|
||||
This product contains 'react-native-keyboard-aware-scroll', a ScrollView component that handles keyboard appearance and automatically scrolls to focused `TextInput`
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/APSL/react-native-keyboard-aware-scroll-view
|
||||
|
||||
* LICENSE :
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 APSL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## harmony-reflect
|
||||
|
||||
[Note: the software referenced below is made available under two licenses. Mattermost, Inc. has elected to license the software pursuant to the Apache License, Version 2.0.]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import Routes from 'app/navigation/routes';
|
||||
import {Constants} from 'service/constants';
|
||||
import {selectPost} from 'service/actions/posts';
|
||||
|
||||
export function goBack() {
|
||||
|
|
@ -181,3 +182,31 @@ export function goToModalAccountSettings() {
|
|||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function goToCreateChannel(channelType) {
|
||||
return async (dispatch, getState) => {
|
||||
closeDrawers()(dispatch, getState);
|
||||
let type;
|
||||
let route;
|
||||
switch (channelType) {
|
||||
case Constants.OPEN_CHANNEL:
|
||||
type = NavigationTypes.NAVIGATION_PUSH;
|
||||
route = Routes.CreatePublicChannel;
|
||||
break;
|
||||
case Constants.PRIVATE_CHANNEL:
|
||||
type = NavigationTypes.NAVIGATION_MODAL;
|
||||
route = Routes.CreatePrivateChannel;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type,
|
||||
route,
|
||||
props: {
|
||||
channelType
|
||||
}
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
29
app/actions/views/create_channel.js
Normal file
29
app/actions/views/create_channel.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {handleSelectChannel} from './channel';
|
||||
import {createChannel} from 'service/actions/channels';
|
||||
import {getCurrentTeamId} from 'service/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'service/selectors/entities/users';
|
||||
import {cleanUpUrlable} from 'service/utils/channel_utils';
|
||||
|
||||
export function handleCreateChannel(displayName, purpose, header, type) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const teamId = getCurrentTeamId(state);
|
||||
let channel = {
|
||||
team_id: teamId,
|
||||
name: cleanUpUrlable(displayName),
|
||||
display_name: displayName,
|
||||
purpose,
|
||||
header,
|
||||
type
|
||||
};
|
||||
|
||||
channel = await createChannel(channel, currentUserId)(dispatch, getState);
|
||||
if (channel && channel.id) {
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -89,6 +89,7 @@ class ChannelDrawerList extends Component {
|
|||
viewChannel: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired,
|
||||
closeDMChannel: PropTypes.func.isRequired,
|
||||
goToCreateChannel: PropTypes.func.isRequired,
|
||||
leaveChannel: PropTypes.func.isRequired,
|
||||
markFavorite: PropTypes.func.isRequired,
|
||||
unmarkFavorite: PropTypes.func.isRequired,
|
||||
|
|
@ -361,6 +362,27 @@ class ChannelDrawerList extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
renderSectionAction = (action) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
style={Styles.more}
|
||||
onPress={action}
|
||||
>
|
||||
<Icon
|
||||
name='plus-circle'
|
||||
size={18}
|
||||
color={theme.sidebarText}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
createPrivateChannel = () => {
|
||||
this.props.actions.goToCreateChannel(Constants.PRIVATE_CHANNEL);
|
||||
};
|
||||
|
||||
buildData = (props) => {
|
||||
const data = [];
|
||||
|
||||
|
|
@ -391,19 +413,6 @@ class ChannelDrawerList extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const moreChannels = (
|
||||
<TouchableHighlight
|
||||
style={Styles.more}
|
||||
onPress={this.props.actions.showMoreChannelsModal}
|
||||
>
|
||||
<Icon
|
||||
name='plus-circle'
|
||||
size={18}
|
||||
color={theme.sidebarText}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
data.push(
|
||||
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
|
||||
<FormattedText
|
||||
|
|
@ -411,31 +420,23 @@ class ChannelDrawerList extends Component {
|
|||
id='sidebar.channels'
|
||||
defaultMessage='CHANNELS'
|
||||
/>
|
||||
{moreChannels}
|
||||
{this.renderSectionAction(this.props.actions.showMoreChannelsModal)}
|
||||
</View>,
|
||||
...publicChannels
|
||||
);
|
||||
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.pg'
|
||||
defaultMessage='PRIVATE GROUPS'
|
||||
/>,
|
||||
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.pg'
|
||||
defaultMessage='PRIVATE GROUPS'
|
||||
/>
|
||||
{this.renderSectionAction(this.createPrivateChannel)}
|
||||
</View>,
|
||||
...privateChannels
|
||||
);
|
||||
|
||||
const moreDms = (
|
||||
<TouchableHighlight
|
||||
style={Styles.more}
|
||||
onPress={this.props.actions.showDirectMessagesModal}
|
||||
>
|
||||
<Icon
|
||||
name='plus-circle'
|
||||
size={18}
|
||||
color={theme.sidebarText}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
data.push(
|
||||
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
|
||||
<FormattedText
|
||||
|
|
@ -443,7 +444,7 @@ class ChannelDrawerList extends Component {
|
|||
id='sidebar.direct'
|
||||
defaultMessage='DIRECT MESSAGES'
|
||||
/>
|
||||
{moreDms}
|
||||
{this.renderSectionAction(this.props.actions.showDirectMessagesModal)}
|
||||
</View>,
|
||||
...directChannels
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {connect} from 'react-redux';
|
|||
|
||||
import {
|
||||
closeModal,
|
||||
goToCreateChannel,
|
||||
showMoreChannelsModal,
|
||||
showDirectMessagesModal,
|
||||
showOptionsModal
|
||||
|
|
@ -33,6 +34,7 @@ function mapDispatchToProps(dispatch) {
|
|||
viewChannel,
|
||||
markChannelAsRead,
|
||||
closeDMChannel,
|
||||
goToCreateChannel,
|
||||
leaveChannel,
|
||||
markFavorite,
|
||||
unmarkFavorite,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
|
||||
import {ActivityIndicator, StyleSheet, View} from 'react-native';
|
||||
|
||||
|
|
@ -17,16 +17,29 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export default class Button extends React.Component {
|
||||
export default class Loading extends PureComponent {
|
||||
static propTypes = {
|
||||
size: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
style: View.propTypes.style
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
size: 'large',
|
||||
color: 'grey',
|
||||
style: {}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator
|
||||
style={styles.loading}
|
||||
style={[styles.loading, this.props.style]}
|
||||
animating={true}
|
||||
size='large'
|
||||
size={this.props.size}
|
||||
color={this.props.color}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ChannelInfo,
|
||||
ChannelMembers,
|
||||
ChannelAddMembers,
|
||||
CreateChannel,
|
||||
LoadTeam,
|
||||
Login,
|
||||
Mfa,
|
||||
|
|
@ -71,6 +72,21 @@ export const Routes = {
|
|||
transition: RouteTransitions.Horizontal,
|
||||
component: ChannelView
|
||||
},
|
||||
CreatePublicChannel: {
|
||||
key: 'CreatePublicChannel',
|
||||
component: CreateChannel,
|
||||
transition: RouteTransitions.Horizontal,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}
|
||||
}
|
||||
},
|
||||
CreatePrivateChannel: {
|
||||
key: 'CreatePrivateChannel',
|
||||
component: CreateChannel,
|
||||
navigationProps: {
|
||||
title: {id: 'mobile.create_channel.private', defaultMessage: 'New Private Group'}
|
||||
}
|
||||
},
|
||||
LoadTeam: {
|
||||
key: 'LoadTeam',
|
||||
component: LoadTeam
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ export default class AccountSettings extends PureComponent {
|
|||
|
||||
renderItems = () => {
|
||||
return [
|
||||
this.buildItemRow('gear', 'user.settings.modal.general', 'General', () => true, true, true),
|
||||
this.buildItemRow('lock', 'user.settings.modal.security', 'Security', () => true, true, true),
|
||||
this.buildItemRow('gear', 'user.settings.modal.general', 'General', () => true, true, false),
|
||||
this.buildItemRow('lock', 'user.settings.modal.security', 'Security', () => true, true, false),
|
||||
this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', () => true, true, false),
|
||||
this.buildItemRow('mobile', 'user.settings.modal.display', 'Display', () => true, true, false),
|
||||
this.buildItemRow('wrench', 'user.settings.modal.advanced', 'Advanced', () => true, false, false)
|
||||
|
|
|
|||
365
app/scenes/create_channel/create_channel.js
Normal file
365
app/scenes/create_channel/create_channel.js
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Dimensions,
|
||||
Keyboard,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
findNodeHandle
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import {Constants, RequestStatus} from 'service/constants';
|
||||
import EventEmitter from 'service/utils/event_emitter';
|
||||
|
||||
import CreateChannelButton from './create_channel_button';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
paddingTop: 30
|
||||
},
|
||||
errorContainer: {
|
||||
position: 'absolute'
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 10
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.newMessageSeparator, 0.5),
|
||||
borderBottomColor: changeOpacity(theme.newMessageSeparator, 0.5),
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
class CreateChannel extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
createChannelRequest: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
channelType: PropTypes.string,
|
||||
subscribeToHeaderEvent: React.PropTypes.func.isRequired,
|
||||
unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
goBack: PropTypes.func.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
handleCreateChannel: PropTypes.func.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
channelType: Constants.OPEN_CHANNEL
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
renderLeftComponent: (props, emitter, theme) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
|
||||
onPress={() => emitter('close')}
|
||||
>
|
||||
<FormattedText
|
||||
id='more_direct_channels.close'
|
||||
defaultMessage='Close'
|
||||
style={{color: theme.sidebarHeaderTextColor}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
renderRightComponent: (props, emitter) => {
|
||||
return <CreateChannelButton emitter={emitter}/>;
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
displayName: '',
|
||||
header: '',
|
||||
purpose: ''
|
||||
};
|
||||
}
|
||||
|
||||
onCreateChannel = async () => {
|
||||
Keyboard.dismiss();
|
||||
const {displayName, purpose, header} = this.state;
|
||||
await 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);
|
||||
}
|
||||
};
|
||||
|
||||
onPurposeChangeText = (purpose) => {
|
||||
this.setState({purpose});
|
||||
};
|
||||
|
||||
onHeaderChangeText = (header) => {
|
||||
this.setState({header});
|
||||
};
|
||||
|
||||
emitCanCreateChannel = (enabled) => {
|
||||
EventEmitter.emit('can_create_channel', enabled);
|
||||
};
|
||||
|
||||
emitCreating = (loading) => {
|
||||
EventEmitter.emit('creating_channel', loading);
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
this.nameInput.refs.wrappedInstance.blur();
|
||||
this.purposeInput.refs.wrappedInstance.blur();
|
||||
this.headerInput.refs.wrappedInstance.blur();
|
||||
this.refs.scroll.scrollToPosition(0, 0, true);
|
||||
};
|
||||
|
||||
channelNameRef = (ref) => {
|
||||
this.nameInput = ref;
|
||||
};
|
||||
|
||||
channelPurposeRef = (ref) => {
|
||||
this.purposeInput = ref;
|
||||
};
|
||||
|
||||
channelHeaderRef = (ref) => {
|
||||
this.headerInput = ref;
|
||||
};
|
||||
|
||||
scrollRef = (ref) => {
|
||||
this.scroll = ref;
|
||||
};
|
||||
|
||||
lastTextRef = (ref) => {
|
||||
this.lastText = ref;
|
||||
};
|
||||
|
||||
scrollToEnd = () => {
|
||||
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
|
||||
this.props.subscribeToHeaderEvent('create_channel', this.onCreateChannel);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {createChannelRequest} = nextProps;
|
||||
|
||||
if (this.props.createChannelRequest !== createChannelRequest) {
|
||||
switch (createChannelRequest.status) {
|
||||
case RequestStatus.STARTED:
|
||||
this.emitCreating(true);
|
||||
this.setState({error: null});
|
||||
break;
|
||||
case RequestStatus.SUCCESS:
|
||||
this.emitCreating(false);
|
||||
this.setState({error: null});
|
||||
this.props.actions.closeModal();
|
||||
break;
|
||||
case RequestStatus.FAILURE:
|
||||
this.emitCreating(false);
|
||||
this.setState({error: createChannelRequest.error});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.emitCanCreateChannel(false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unsubscribeFromHeaderEvent('close');
|
||||
this.props.unsubscribeFromHeaderEvent('create_channel');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {channelType, theme} = this.props;
|
||||
const {displayName, header, purpose, error} = this.state;
|
||||
const {formatMessage} = this.props.intl;
|
||||
const {height, width} = Dimensions.get('window');
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let term;
|
||||
if (channelType === Constants.OPEN_CHANNEL) {
|
||||
term = formatMessage({id: 'channel_modal.channel', defaultMessage: 'Channel'});
|
||||
} else if (channelType === Constants.PRIVATE_CHANNEL) {
|
||||
term = formatMessage({id: 'channel_modal.group', defaultMessage: 'Group'});
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<View style={[style.errorContainer, {width}]}>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText error={error}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAwareScrollView
|
||||
ref={this.scrollRef}
|
||||
style={style.container}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
<View style={[style.scrollView, {height: height + (Platform.OS === 'android' ? 200 : 0)}]}>
|
||||
{displayError}
|
||||
<View>
|
||||
<FormattedText
|
||||
style={[style.title, {marginTop: (error ? 10 : 0)}]}
|
||||
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(theme.centerChannelColor, 0.4)}
|
||||
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}]}
|
||||
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
multiline={true}
|
||||
underlineColorAndroid='transparent'
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={style.helpText}
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this {term} should be used.'
|
||||
values={{
|
||||
term
|
||||
}}
|
||||
/>
|
||||
</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}]}
|
||||
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.4)}
|
||||
multiline={true}
|
||||
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 {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).'}
|
||||
values={{
|
||||
term
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAwareScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(CreateChannel);
|
||||
107
app/scenes/create_channel/create_channel_button.js
Normal file
107
app/scenes/create_channel/create_channel_button.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import EventEmitter from 'service/utils/event_emitter';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
class CreateChannelButton extends PureComponent {
|
||||
static propTypes = {
|
||||
emitter: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
theme: {}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enabled: false,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
EventEmitter.on('can_create_channel', this.onCanCreate);
|
||||
EventEmitter.on('creating_channel', this.onLoading);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('can_create_channel', this.onCanCreate);
|
||||
EventEmitter.off('creating_channel', this.onLoading);
|
||||
}
|
||||
|
||||
onCanCreate = (enabled) => {
|
||||
this.setState({enabled});
|
||||
};
|
||||
|
||||
onLoading = (loading) => {
|
||||
this.setState({loading});
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
if (this.state.enabled) {
|
||||
this.props.emitter('create_channel');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const {enabled, loading} = this.state;
|
||||
let color = changeOpacity(theme.sidebarHeaderTextColor, 0.4);
|
||||
if (enabled) {
|
||||
color = theme.sidebarHeaderTextColor;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Loading
|
||||
color={color}
|
||||
size='small'
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
|
||||
<TouchableOpacity
|
||||
onPress={this.onPress}
|
||||
style={{paddingHorizontal: 15}}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.create_channel'
|
||||
defaultMessage='Create'
|
||||
style={{color}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CreateChannelButton);
|
||||
36
app/scenes/create_channel/index.js
Normal file
36
app/scenes/create_channel/index.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goBack, closeModal} from 'app/actions/navigation';
|
||||
import {handleCreateChannel} from 'app/actions/views/create_channel';
|
||||
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
|
||||
import CreateChannel from './create_channel';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {createChannel: createChannelRequest} = state.requests.channels;
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
createChannelRequest,
|
||||
channelType: ownProps.channelType,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
goBack,
|
||||
closeModal,
|
||||
handleCreateChannel
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(CreateChannel);
|
||||
|
|
@ -7,6 +7,7 @@ import ChannelDrawer from './channel_drawer';
|
|||
import ChannelInfo from './channel_info';
|
||||
import ChannelMembers from './channel_members';
|
||||
import ChannelAddMembers from './channel_add_members';
|
||||
import CreateChannel from './create_channel';
|
||||
import LoadTeam from './load_team';
|
||||
import Login from './login/login_container.js';
|
||||
import Mfa from './mfa';
|
||||
|
|
@ -28,6 +29,7 @@ module.exports = {
|
|||
ChannelInfo,
|
||||
ChannelMembers,
|
||||
ChannelAddMembers,
|
||||
CreateChannel,
|
||||
LoadTeam,
|
||||
Login,
|
||||
Mfa,
|
||||
|
|
|
|||
47
app/scenes/more_channels/create_button.js
Normal file
47
app/scenes/more_channels/create_button.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
|
||||
function CreateButton(props) {
|
||||
return (
|
||||
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
|
||||
<TouchableOpacity
|
||||
onPress={() => props.emitter('new_channel')}
|
||||
style={{paddingHorizontal: 15}}
|
||||
>
|
||||
<FormattedText
|
||||
id='mobile.create_channel'
|
||||
defaultMessage='Create'
|
||||
style={{color: props.theme.sidebarHeaderTextColor}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
CreateButton.propTypes = {
|
||||
emitter: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
CreateButton.defaultProps = {
|
||||
theme: {}
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CreateButton);
|
||||
|
|
@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
|||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goBack} from 'app/actions/navigation';
|
||||
import {goBack, goToCreateChannel} from 'app/actions/navigation';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getMoreChannels as getMoreChannelsSelector} from 'service/selectors/entities/channels';
|
||||
import {handleSelectChannel} from 'app/actions/views/channel';
|
||||
|
|
@ -32,6 +32,7 @@ function mapDispatchToProps(dispatch) {
|
|||
actions: bindActionCreators({
|
||||
goBack,
|
||||
handleSelectChannel,
|
||||
goToCreateChannel,
|
||||
joinChannel,
|
||||
getMoreChannels,
|
||||
searchMoreChannels
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import SearchBar from 'app/components/search_bar';
|
|||
import {Constants, RequestStatus} from 'service/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import CreateButton from './create_button';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
|
|
@ -40,6 +42,7 @@ class MoreChannels extends PureComponent {
|
|||
actions: PropTypes.shape({
|
||||
goBack: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
goToCreateChannel: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
getMoreChannels: PropTypes.func.isRequired,
|
||||
searchMoreChannels: PropTypes.func.isRequired
|
||||
|
|
@ -60,6 +63,9 @@ class MoreChannels extends PureComponent {
|
|||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
},
|
||||
renderRightComponent: (props, emitter) => {
|
||||
return <CreateButton emitter={emitter}/>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -78,6 +84,7 @@ class MoreChannels extends PureComponent {
|
|||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
|
||||
this.props.subscribeToHeaderEvent('new_channel', this.onCreateChannel);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
|
@ -102,6 +109,7 @@ class MoreChannels extends PureComponent {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.props.unsubscribeFromHeaderEvent('close');
|
||||
this.props.unsubscribeFromHeaderEvent('new_channel');
|
||||
}
|
||||
|
||||
filterChannels = (channels, term) => {
|
||||
|
|
@ -197,6 +205,10 @@ class MoreChannels extends PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
onCreateChannel = async () => {
|
||||
this.props.actions.goToCreateChannel(Constants.OPEN_CHANNEL);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const isLoading = this.props.requestStatus.status === RequestStatus.STARTED;
|
||||
|
|
|
|||
|
|
@ -1025,6 +1025,7 @@
|
|||
"channel_modal.group": "Group",
|
||||
"channel_modal.header": "Header",
|
||||
"channel_modal.headerHelp": "Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).",
|
||||
"channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
|
||||
"channel_modal.modalTitle": "New ",
|
||||
"channel_modal.name": "Name",
|
||||
"channel_modal.nameEx": "E.g.: \"Bugs\", \"Marketing\", \"客户支持\"",
|
||||
|
|
@ -1034,6 +1035,7 @@
|
|||
"channel_modal.publicChannel1": "Create a public channel",
|
||||
"channel_modal.publicChannel2": "Create a new public channel anyone can join. ",
|
||||
"channel_modal.purpose": "Purpose",
|
||||
"channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
|
||||
"channel_notifications.allActivity": "For all activity",
|
||||
"channel_notifications.allUnread": "For all unread messages",
|
||||
"channel_notifications.globalDefault": "Global default ({notifyLevel})",
|
||||
|
|
@ -1503,6 +1505,9 @@
|
|||
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
|
||||
"mobile.components.select_server_view.continue": "Continue",
|
||||
"mobile.components.select_server_view.siteUrlPlaceholder": "https://mattermost.example.com",
|
||||
"mobile.create_channel": "Create",
|
||||
"mobile.create_channel.public": "New Public Channel",
|
||||
"mobile.create_channel.private": "New Private Group",
|
||||
"mobile.loading_channels": "Loading Channels...",
|
||||
"mobile.loading_members": "Loading Members...",
|
||||
"mobile.routes.channels": "Channels",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"react-native": "0.40.0",
|
||||
"react-native-button": "1.7.1",
|
||||
"react-native-drawer": "2.3.0",
|
||||
"react-native-keyboard-aware-scroll-view": "0.2.7",
|
||||
"react-native-keyboard-spacer": "0.3.1",
|
||||
"react-native-search-bar": "enahum/react-native-search-bar.git",
|
||||
"react-native-svg": "4.5.0",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function createChannel(channel, userId) {
|
|||
error
|
||||
}
|
||||
]), getState);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const member = {
|
||||
|
|
@ -77,6 +77,8 @@ export function createChannel(channel, userId) {
|
|||
type: ChannelTypes.CHANNEL_MEMBERS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
|
||||
return created;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import fetch from './fetch_etag';
|
||||
|
||||
import EventEmitter from 'service/utils/event_emitter';
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
|
|
|
|||
38
service/client/fetch_etag.js
Normal file
38
service/client/fetch_etag.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
const data = {};
|
||||
const etags = {};
|
||||
|
||||
export default (url = null, options = {headers: {}}) => {
|
||||
url = url || options.url; // eslint-disable-line no-param-reassign
|
||||
|
||||
if (options.method === 'GET' || !options.method) {
|
||||
const etag = etags[url];
|
||||
const cachedResponse = data[`${url}${etag}`]; // ensure etag is for url
|
||||
if (etag) {
|
||||
options.headers['If-None-Match'] = etag;
|
||||
}
|
||||
|
||||
return fetch(url, options).
|
||||
then((response) => {
|
||||
if (response.status === 304) {
|
||||
return cachedResponse.clone();
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
const responseEtag = response.headers.get('Etag');
|
||||
|
||||
if (responseEtag) {
|
||||
data[`${url}${responseEtag}`] = response.clone();
|
||||
etags[url] = responseEtag;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// all other requests go straight to fetch
|
||||
return Reflect.apply(fetch, undefined, [url, options]); //eslint-disable-line no-undefined
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ChannelTypes, UsersTypes} from 'service/constants';
|
||||
import {ChannelTypes, TeamsTypes, UsersTypes} from 'service/constants';
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
function currentId(state = '', action) {
|
||||
|
|
@ -71,6 +71,7 @@ function channels(state = {}, action) {
|
|||
};
|
||||
}
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
case TeamsTypes.SELECT_TEAM:
|
||||
return {};
|
||||
|
||||
default:
|
||||
|
|
@ -126,6 +127,7 @@ function myMembers(state = {}, action) {
|
|||
return nextState;
|
||||
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
case TeamsTypes.SELECT_TEAM:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
|
|
@ -142,6 +144,7 @@ function stats(state = {}, action) {
|
|||
return nextState;
|
||||
}
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
case TeamsTypes.SELECT_TEAM:
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
|
|
|
|||
|
|
@ -177,3 +177,11 @@ function not(f) {
|
|||
function andX(...fns) {
|
||||
return (...args) => fns.every((f) => f(...args));
|
||||
}
|
||||
|
||||
export function cleanUpUrlable(input) {
|
||||
let cleaned = input.trim().replace(/-/g, ' ').replace(/[^\w\s]/gi, '').toLowerCase().replace(/\s/g, '-');
|
||||
cleaned = cleaned.replace(/-{2,}/, '-');
|
||||
cleaned = cleaned.replace(/^-+/, '');
|
||||
cleaned = cleaned.replace(/-+$/, '');
|
||||
return cleaned;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,8 +212,8 @@ describe('Actions.Websocket', () => {
|
|||
|
||||
it('Websocket Handle Channel Deleted', (done) => {
|
||||
async function test() {
|
||||
await ChannelActions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState);
|
||||
await TeamActions.selectTeam(TestHelper.basicTeam)(store.dispatch, store.getState);
|
||||
await ChannelActions.fetchMyChannelsAndMembers(TestHelper.basicTeam.id)(store.dispatch, store.getState);
|
||||
await ChannelActions.selectChannel(TestHelper.basicChannel.id)(store.dispatch, store.getState);
|
||||
await Client.deleteChannel(
|
||||
TestHelper.basicTeam.id,
|
||||
|
|
|
|||
Loading…
Reference in a new issue