diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/create_post.jsx | 62 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 68 | ||||
-rw-r--r-- | web/react/components/sidebar_header.jsx | 89 | ||||
-rw-r--r-- | web/react/components/tutorial/tutorial_intro_screens.jsx | 14 | ||||
-rw-r--r-- | web/react/components/tutorial/tutorial_tip.jsx | 108 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 11 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_tutorial.scss | 21 | ||||
-rw-r--r-- | web/sass-files/sass/styles.scss | 2 |
8 files changed, 343 insertions, 32 deletions
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 7c601af4b..1dc30e251 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -1,21 +1,26 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +const MsgTyping = require('./msg_typing.jsx'); +const Textbox = require('./textbox.jsx'); +const FileUpload = require('./file_upload.jsx'); +const FilePreview = require('./file_preview.jsx'); +const TutorialTip = require('./tutorial/tutorial_tip.jsx'); + const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); const Client = require('../utils/client.jsx'); const AsyncClient = require('../utils/async_client.jsx'); +const Utils = require('../utils/utils.jsx'); + const ChannelStore = require('../stores/channel_store.jsx'); const PostStore = require('../stores/post_store.jsx'); const UserStore = require('../stores/user_store.jsx'); -const SocketStore = require('../stores/socket_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); -const MsgTyping = require('./msg_typing.jsx'); -const Textbox = require('./textbox.jsx'); -const FileUpload = require('./file_upload.jsx'); -const FilePreview = require('./file_preview.jsx'); -const Utils = require('../utils/utils.jsx'); +const SocketStore = require('../stores/socket_store.jsx'); const Constants = require('../utils/constants.jsx'); +const Preferences = Constants.Preferences; +const TutorialSteps = Constants.TutorialSteps; const ActionTypes = Constants.ActionTypes; const KeyCodes = Constants.KeyCodes; @@ -36,15 +41,16 @@ export default class CreatePost extends React.Component { this.handleTextDrop = this.handleTextDrop.bind(this); this.removePreview = this.removePreview.bind(this); this.onChange = this.onChange.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); this.getFileCount = this.getFileCount.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleResize = this.handleResize.bind(this); this.sendMessage = this.sendMessage.bind(this); - this.onPreferenceChange = this.onPreferenceChange.bind(this); PostStore.clearDraftUploads(); const draft = this.getCurrentDraft(); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); this.state = { channelId: ChannelStore.getCurrentId(), @@ -55,16 +61,12 @@ export default class CreatePost extends React.Component { initialText: draft.messageText, windowWidth: Utils.windowWidth(), windowHeight: Utils.windowHeight(), - ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value, + showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER }; PreferenceStore.addChangeListener(this.onPreferenceChange); } - onPreferenceChange() { - this.setState({ - ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value - }); - } handleResize() { this.setState({ windowWidth: Utils.windowWidth(), @@ -318,11 +320,13 @@ export default class CreatePost extends React.Component { } componentDidMount() { ChannelStore.addChangeListener(this.onChange); + PreferenceStore.addChangeListener(this.onPreferenceChange); this.resizePostHolder(); window.addEventListener('resize', this.handleResize); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); + PreferenceStore.removeChangeListener(this.onPreferenceChange); window.removeEventListener('resize', this.handleResize); } onChange() { @@ -333,6 +337,13 @@ export default class CreatePost extends React.Component { this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress}); } } + onPreferenceChange() { + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + this.setState({ + showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.POST_POPOVER, + ctrlSend: PreferenceStore.getPreference(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter', {value: 'false'}).value + }); + } getFileCount(channelId) { if (channelId === this.state.channelId) { return this.state.previews.length + this.state.uploadsInProgress.length; @@ -367,6 +378,25 @@ export default class CreatePost extends React.Component { }); } } + createTutorialTip() { + const screens = []; + + screens.push( + <div> + <h4><strong>{'Sending Messages'}</strong></h4> + {'Type here to write a message.'} + <br/><br/> + {'Click the attachment button to upload an image or a file.'} + </div> + ); + + return ( + <TutorialTip + placement='top' + screens={screens} + /> + ); + } render() { let serverError = null; if (this.state.serverError) { @@ -398,6 +428,11 @@ export default class CreatePost extends React.Component { postFooterClassName += ' has-error'; } + let tutorialTip = null; + if (this.state.showTutorialTip) { + tutorialTip = this.createTutorialTip(); + } + return ( <form id='create_post' @@ -436,6 +471,7 @@ export default class CreatePost extends React.Component { > <i className='fa fa-paper-plane' /> </a> + {tutorialTip} </div> <div className={postFooterClassName}> {postError} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 023955e97..c3f43ff69 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -1,19 +1,26 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const AsyncClient = require('../utils/async_client.jsx'); -const ChannelStore = require('../stores/channel_store.jsx'); -const Client = require('../utils/client.jsx'); -const Constants = require('../utils/constants.jsx'); -const PreferenceStore = require('../stores/preference_store.jsx'); const NewChannelFlow = require('./new_channel_flow.jsx'); const MoreDirectChannels = require('./more_direct_channels.jsx'); const SearchBox = require('./search_bar.jsx'); const SidebarHeader = require('./sidebar_header.jsx'); -const TeamStore = require('../stores/team_store.jsx'); const UnreadChannelIndicator = require('./unread_channel_indicator.jsx'); +const TutorialTip = require('./tutorial/tutorial_tip.jsx'); + +const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); + +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); const Utils = require('../utils/utils.jsx'); + +const Constants = require('../utils/constants.jsx'); +const Preferences = Constants.Preferences; +const TutorialSteps = Constants.TutorialSteps; + const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -155,12 +162,15 @@ export default class Sidebar extends React.Component { visibleDirectChannels.sort(this.sortChannelsByDisplayName); + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + return { activeId: currentId, channels: ChannelStore.getAll(), members, visibleDirectChannels, - hiddenDirectChannelCount + hiddenDirectChannelCount, + showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.CHANNEL_POPOVER }; } @@ -308,6 +318,44 @@ export default class Sidebar extends React.Component { this.setState({showDirectChannelsModal: false}); } + createTutorialTip() { + const screens = []; + + screens.push( + <div> + <h4><strong>{'Channels'}</strong></h4> + <strong>{'Channels'}</strong>{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}<strong>{'Direct Messages'}</strong>{' for a single person or '}<strong>{'Private Groups'}</strong>{' for multiple people.'} + </div> + ); + + screens.push( + <div> + <h4><strong>{'"Town Square" and "Off-Topic" channels'}</strong></h4> + {'Here are two public channels to start:'} + <br/><br/> + <strong>{'Town Square'}</strong>{' is a place for team-wide communication. Everyone in your team is a member of this channel.'} + <br/><br/> + <strong>{'Off-Topic'}</strong>{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'} + </div> + ); + + screens.push( + <div> + <h4><strong>{'Creating and Joining Channels'}</strong></h4> + {'Click '}<strong>{'"More..."'}</strong>{' to create a new channel or join an existing one.'} + <br/><br/> + {'You can also create a new channel or private group by clicking the '}<strong>{'"+" symbol'}</strong>{' next to the channel or private group header.'} + </div> + ); + + return ( + <TutorialTip + placement='right' + screens={screens} + /> + ); + } + createChannelElement(channel, index, arr, handleClose) { var members = this.state.members; var activeId = this.state.activeId; @@ -444,6 +492,11 @@ export default class Sidebar extends React.Component { rowClass += ' has-close'; } + let tutorialTip = null; + if (this.state.showTutorialTip && channel.name === Constants.DEFAULT_CHANNEL) { + tutorialTip = this.createTutorialTip(); + } + return ( <li key={channel.name} @@ -460,6 +513,7 @@ export default class Sidebar extends React.Component { {badge} {closeButton} </a> + {tutorialTip} </li> ); } diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 65e4c6d7e..96348f688 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -1,9 +1,16 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var NavbarDropdown = require('./navbar_dropdown.jsx'); -var UserStore = require('../stores/user_store.jsx'); +const NavbarDropdown = require('./navbar_dropdown.jsx'); +const TutorialTip = require('./tutorial/tutorial_tip.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); + const Utils = require('../utils/utils.jsx'); +const Constants = require('../utils/constants.jsx'); +const Preferences = Constants.Preferences; +const TutorialSteps = Constants.TutorialSteps; const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -13,8 +20,23 @@ export default class SidebarHeader extends React.Component { super(props); this.toggleDropdown = this.toggleDropdown.bind(this); + this.onPreferenceChange = this.onPreferenceChange.bind(this); - this.state = {}; + this.state = this.getStateFromStores(); + } + componentDidMount() { + PreferenceStore.addChangeListener(this.onPreferenceChange); + } + componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); + } + getStateFromStores() { + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + + return {showTutorialTip: parseInt(tutorialPref.value, 10) === TutorialSteps.MENU_POPOVER}; + } + onPreferenceChange() { + this.setState(this.getStateFromStores()); } toggleDropdown(e) { e.preventDefault(); @@ -22,8 +44,63 @@ export default class SidebarHeader extends React.Component { this.refs.dropdown.blockToggle = false; return; } + console.log(this.refs.tip); + this.refs.tip.toggle(); $('.team__header').find('.dropdown-toggle').dropdown('toggle'); } + createTutorialTip() { + const screens = []; + + let teamSettingsLink = <strong>{'Team Settings'}</strong>; + if (Utils.isAdmin(UserStore.getCurrentUser().roles)) { + teamSettingsLink = ( + <a + href='#' + data-toggle='modal' + data-target='#team_settings' + > + {'Team Settings'} + </a> + ); + } + + screens.push( + <div> + <h4><strong>{'Sending Messages'}</strong></h4> + {'The '}<strong>{'Main Menu'}</strong>{' is where you can '} + <a + href='#' + data-toggle='modal' + data-target='#invite_member' + > + {'Invite New Members'} + </a> + {', access your '} + <a + href='#' + data-toggle='modal' + data-target='#user_settings' + > + {'Account Settings'} + </a> + {', and set your '}<strong>{'Theme Color'}</strong>{'.'} + <br/><br/> + {'Team administrators can also access their '}{teamSettingsLink}{' from this menu.'} + </div> + ); + + return ( + <div + onClick={this.toggleDropdown} + > + <TutorialTip + ref='tip' + placement='right' + screens={screens} + /> + </div> + ); + } render() { var me = UserStore.getCurrentUser(); var profilePicture = null; @@ -41,8 +118,14 @@ export default class SidebarHeader extends React.Component { ); } + let tutorialTip = null; + if (this.state.showTutorialTip) { + tutorialTip = this.createTutorialTip(); + } + return ( <div className='team__header theme'> + {tutorialTip} <a href='#' onClick={this.toggleDropdown} diff --git a/web/react/components/tutorial/tutorial_intro_screens.jsx b/web/react/components/tutorial/tutorial_intro_screens.jsx index d7568e4cd..d423b4f1b 100644 --- a/web/react/components/tutorial/tutorial_intro_screens.jsx +++ b/web/react/components/tutorial/tutorial_intro_screens.jsx @@ -18,21 +18,25 @@ export default class TutorialIntroScreens extends React.Component { this.handleNext = this.handleNext.bind(this); this.createScreen = this.createScreen.bind(this); - this.state = {screen: 0}; + this.state = {currentScreen: 0}; } handleNext() { - if (this.state.screen < 2) { - this.setState({screen: this.state.screen + 1}); + if (this.state.currentScreen < 2) { + this.setState({currentScreen: this.state.currentScreen + 1}); return; } Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); - const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_INTRO_COMPLETE, UserStore.getCurrentId(), 'true'); + let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + + const newValue = (parseInt(preference.value, 10) + 1).toString(); + + preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); AsyncClient.savePreferences([preference]); } createScreen() { - switch (this.state.screen) { + switch (this.state.currentScreen) { case 0: return this.createScreenOne(); case 1: diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx new file mode 100644 index 000000000..166d10d8a --- /dev/null +++ b/web/react/components/tutorial/tutorial_tip.jsx @@ -0,0 +1,108 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const UserStore = require('../../stores/user_store.jsx'); +const PreferenceStore = require('../../stores/preference_store.jsx'); +const AsyncClient = require('../../utils/async_client.jsx'); + +const Constants = require('../../utils/constants.jsx'); +const Preferences = Constants.Preferences; + +const Overlay = ReactBootstrap.Overlay; + +export default class TutorialTip extends React.Component { + constructor(props) { + super(props); + + this.handleNext = this.handleNext.bind(this); + this.toggle = this.toggle.bind(this); + + this.state = {currentScreen: 0, show: false}; + } + toggle() { + const show = !this.state.show; + this.setState({show}); + + if (!show && this.state.currentScreen >= this.props.screens.length - 1) { + let preference = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + + const newValue = (parseInt(preference.value, 10) + 1).toString(); + + preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), newValue); + AsyncClient.savePreferences([preference]); + } + } + handleNext() { + if (this.state.currentScreen < this.props.screens.length - 1) { + this.setState({currentScreen: this.state.currentScreen + 1}); + return; + } + + this.toggle(); + } + skipTutorial(e) { + e.preventDefault(); + const preference = PreferenceStore.setPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), '999'); + AsyncClient.savePreferences([preference]); + } + render() { + const buttonText = this.state.currentScreen === this.props.screens.length - 1 ? 'Okay' : 'Next'; + + const dots = []; + if (this.props.screens.length > 1) { + for (let i = 0; i < this.props.screens.length; i++) { + if (i === this.state.currentScreen) { + dots.push(<span key={'dotactive' + i}>{'[ x ]'}</span>); + } else { + dots.push(<span key={'dotinactive' + i}>{'[ ]'}</span>); + } + } + } + + return ( + <div className='tip-div'> + <img + className='tip-button' + src='/static/images/next.png' + onClick={this.toggle} + ref='target' + /> + + <Overlay + placement={this.props.placement} + show={this.state.show} + rootClose={true} + onHide={this.toggle} + target={() => this.refs.target} + > + <div className='tip-overlay'> + {this.props.screens[this.state.currentScreen]} + <br/> + {dots} + <button + className='btn btn-default' + onClick={this.handleNext} + > + {buttonText} + </button> + <br/> + <span> + {'Seen this before? '} + <a + href='#' + onClick={this.skipTutorial} + > + {'Opt out of these tips.'} + </a> + </span> + </div> + </Overlay> + </div> + ); + } +} + +TutorialTip.propTypes = { + screens: React.PropTypes.array.isRequired, + placement: React.PropTypes.string.isRequired +}; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 57be5046e..fd64b1554 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -314,9 +314,14 @@ module.exports = { Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', CATEGORY_DISPLAY_SETTINGS: 'display_settings', - CATEGORY_ADVANCED_SETTINGS: 'advanced_settings' - TUTORIAL_INTRO_COMPLETE: 'tutorial_intro_complete', - TUTORIAL_POPOVERS: 'tutorial_popovers' + CATEGORY_ADVANCED_SETTINGS: 'advanced_settings', + TUTORIAL_STEP: 'tutorial_step' + }, + TutorialSteps: { + INTRO_SCREENS: 0, + POST_POPOVER: 1, + CHANNEL_POPOVER: 2, + MENU_POPOVER: 3 }, KeyCodes: { UP: 38, diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss new file mode 100644 index 000000000..e80abf952 --- /dev/null +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -0,0 +1,21 @@ +.tip-overlay { + position:absolute; + background-color:#EEE; + box-shadow:0 5px 10px rgba(0, 0, 0, 0.2); + border:1px solid #CCC; + border-radius:3px; + padding:10px; + z-index:999; +} + +.tip-button { + height:20px; + width:20px; + z-index:998; +} + +.tip-div { + position:absolute; + top:0px; + right:0px; +} diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index ad2cae194..34f7c6eed 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -38,7 +38,7 @@ @import "partials/loading"; @import "partials/get-link"; @import "partials/markdown"; -@import "partials/statistics"; +@import "partials/tutorial"; // Responsive Css @import "partials/responsive"; |