PLT-5587 Create channel (#300)

* PLT-5587 Create channel

* Feedback review

* hidding options in account settings
This commit is contained in:
enahum 2017-02-27 18:17:18 -03:00 committed by Harrison Healey
parent 303574f235
commit c121097974
23 changed files with 795 additions and 43 deletions

View file

@ -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.]

View file

@ -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);
};
}

View 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);
}
};
}

View file

@ -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
);

View file

@ -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,

View file

@ -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>
);
}
}
}

View file

@ -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

View file

@ -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)

View 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);

View 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);

View 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);

View file

@ -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,

View 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);

View file

@ -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

View file

@ -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;

View file

@ -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",

View file

@ -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",

View file

@ -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;
};
}

View file

@ -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';

View 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
};

View file

@ -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;

View file

@ -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;
}

View file

@ -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,