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,