diff --git a/NOTICE.txt b/NOTICE.txt
index 96d68a2d2..9275ce2e6 100644
--- a/NOTICE.txt
+++ b/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.]
diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js
index 5a4b79bf5..88ff1c070 100644
--- a/app/actions/navigation/index.js
+++ b/app/actions/navigation/index.js
@@ -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);
+ };
+}
diff --git a/app/actions/views/create_channel.js b/app/actions/views/create_channel.js
new file mode 100644
index 000000000..db465d82c
--- /dev/null
+++ b/app/actions/views/create_channel.js
@@ -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);
+ }
+ };
+}
diff --git a/app/components/channel_drawer_list/channel_drawer_list.js b/app/components/channel_drawer_list/channel_drawer_list.js
index f652371b6..d26e45af3 100644
--- a/app/components/channel_drawer_list/channel_drawer_list.js
+++ b/app/components/channel_drawer_list/channel_drawer_list.js
@@ -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 (
+
+
+
+ );
+ };
+
+ createPrivateChannel = () => {
+ this.props.actions.goToCreateChannel(Constants.PRIVATE_CHANNEL);
+ };
+
buildData = (props) => {
const data = [];
@@ -391,19 +413,6 @@ class ChannelDrawerList extends Component {
);
}
- const moreChannels = (
-
-
-
- );
-
data.push(
- {moreChannels}
+ {this.renderSectionAction(this.props.actions.showMoreChannelsModal)}
,
...publicChannels
);
+
data.push(
- ,
+
+
+ {this.renderSectionAction(this.createPrivateChannel)}
+ ,
...privateChannels
);
- const moreDms = (
-
-
-
- );
data.push(
- {moreDms}
+ {this.renderSectionAction(this.props.actions.showDirectMessagesModal)}
,
...directChannels
);
diff --git a/app/components/channel_drawer_list/channel_drawer_list_container.js b/app/components/channel_drawer_list/channel_drawer_list_container.js
index e316e1da4..57fd07e32 100644
--- a/app/components/channel_drawer_list/channel_drawer_list_container.js
+++ b/app/components/channel_drawer_list/channel_drawer_list_container.js
@@ -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,
diff --git a/app/components/loading.js b/app/components/loading.js
index 6925fbf43..1f875d7a9 100644
--- a/app/components/loading.js
+++ b/app/components/loading.js
@@ -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 (
);
}
-}
\ No newline at end of file
+}
diff --git a/app/navigation/routes.js b/app/navigation/routes.js
index 21c12f5c5..0260631bd 100644
--- a/app/navigation/routes.js
+++ b/app/navigation/routes.js
@@ -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
diff --git a/app/scenes/account_settings/account_settings.js b/app/scenes/account_settings/account_settings.js
index 957bf2628..c61510b42 100644
--- a/app/scenes/account_settings/account_settings.js
+++ b/app/scenes/account_settings/account_settings.js
@@ -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)
diff --git a/app/scenes/create_channel/create_channel.js b/app/scenes/create_channel/create_channel.js
new file mode 100644
index 000000000..973fcc1ee
--- /dev/null
+++ b/app/scenes/create_channel/create_channel.js
@@ -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 (
+ emitter('close')}
+ >
+
+
+ );
+ },
+ renderRightComponent: (props, emitter) => {
+ return ;
+ }
+ };
+
+ 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 = (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {displayError}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default injectIntl(CreateChannel);
diff --git a/app/scenes/create_channel/create_channel_button.js b/app/scenes/create_channel/create_channel_button.js
new file mode 100644
index 000000000..23a262a1a
--- /dev/null
+++ b/app/scenes/create_channel/create_channel_button.js
@@ -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 (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(CreateChannelButton);
diff --git a/app/scenes/create_channel/index.js b/app/scenes/create_channel/index.js
new file mode 100644
index 000000000..5ca450970
--- /dev/null
+++ b/app/scenes/create_channel/index.js
@@ -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);
diff --git a/app/scenes/index.js b/app/scenes/index.js
index c3cfc1e5c..6b5829850 100644
--- a/app/scenes/index.js
+++ b/app/scenes/index.js
@@ -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,
diff --git a/app/scenes/more_channels/create_button.js b/app/scenes/more_channels/create_button.js
new file mode 100644
index 000000000..579ae90cd
--- /dev/null
+++ b/app/scenes/more_channels/create_button.js
@@ -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 (
+
+ props.emitter('new_channel')}
+ style={{paddingHorizontal: 15}}
+ >
+
+
+
+ );
+}
+
+CreateButton.propTypes = {
+ emitter: PropTypes.func.isRequired,
+ theme: PropTypes.object
+};
+
+CreateButton.defaultProps = {
+ theme: {}
+};
+
+function mapStateToProps(state) {
+ return {
+ theme: getTheme(state)
+ };
+}
+
+export default connect(mapStateToProps)(CreateButton);
diff --git a/app/scenes/more_channels/index.js b/app/scenes/more_channels/index.js
index 0e12b9901..9850c9e93 100644
--- a/app/scenes/more_channels/index.js
+++ b/app/scenes/more_channels/index.js
@@ -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
diff --git a/app/scenes/more_channels/more_channels.js b/app/scenes/more_channels/more_channels.js
index 232b8ef70..1369e4992 100644
--- a/app/scenes/more_channels/more_channels.js
+++ b/app/scenes/more_channels/more_channels.js
@@ -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 {
/>
);
+ },
+ renderRightComponent: (props, emitter) => {
+ return ;
}
};
@@ -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;
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index ec5914707..20de09719 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/package.json b/package.json
index 84e425e28..e8e4d57d3 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/service/actions/channels.js b/service/actions/channels.js
index b52b66e47..68086f1d5 100644
--- a/service/actions/channels.js
+++ b/service/actions/channels.js
@@ -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;
};
}
diff --git a/service/client/client.js b/service/client/client.js
index 6f03bf7b2..799dd840e 100644
--- a/service/client/client.js
+++ b/service/client/client.js
@@ -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';
diff --git a/service/client/fetch_etag.js b/service/client/fetch_etag.js
new file mode 100644
index 000000000..81e67f655
--- /dev/null
+++ b/service/client/fetch_etag.js
@@ -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
+};
diff --git a/service/reducers/entities/channels.js b/service/reducers/entities/channels.js
index d6d7e441e..6d9cd76f0 100644
--- a/service/reducers/entities/channels.js
+++ b/service/reducers/entities/channels.js
@@ -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;
diff --git a/service/utils/channel_utils.js b/service/utils/channel_utils.js
index 9ac7d9731..8e4eff95f 100644
--- a/service/utils/channel_utils.js
+++ b/service/utils/channel_utils.js
@@ -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;
+}
diff --git a/test/service/actions/websocket.test.js b/test/service/actions/websocket.test.js
index e1ff8fb8b..6310430eb 100644
--- a/test/service/actions/websocket.test.js
+++ b/test/service/actions/websocket.test.js
@@ -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,