diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/react/components/center_panel.jsx | 42 | ||||
-rw-r--r-- | web/react/components/channel_view.jsx | 8 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 62 | ||||
-rw-r--r-- | web/react/components/navbar_dropdown.jsx | 2 | ||||
-rw-r--r-- | web/react/components/posts_view_container.jsx | 9 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 75 | ||||
-rw-r--r-- | web/react/components/sidebar_header.jsx | 66 | ||||
-rw-r--r-- | web/react/components/tutorial/tutorial_intro_screens.jsx | 152 | ||||
-rw-r--r-- | web/react/components/tutorial/tutorial_tip.jsx | 131 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 9 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_responsive.scss | 22 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_tutorial.scss | 188 | ||||
-rw-r--r-- | web/sass-files/sass/styles.scss | 1 | ||||
-rw-r--r-- | web/static/images/tutorialTip.gif | bin | 0 -> 18421 bytes |
14 files changed, 728 insertions, 39 deletions
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index b871fe81a..242c2c637 100644 --- a/web/react/components/center_panel.jsx +++ b/web/react/components/center_panel.jsx @@ -1,17 +1,47 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var CreatePost = require('../components/create_post.jsx'); -var PostsViewContainer = require('../components/posts_view_container.jsx'); -var ChannelHeader = require('../components/channel_header.jsx'); -var Navbar = require('../components/navbar.jsx'); -var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); +const TutorialIntroScreens = require('./tutorial/tutorial_intro_screens.jsx'); +const CreatePost = require('./create_post.jsx'); +const PostsViewContainer = require('./posts_view_container.jsx'); +const ChannelHeader = require('./channel_header.jsx'); +const Navbar = require('./navbar.jsx'); +const FileUploadOverlay = require('./file_upload_overlay.jsx'); + +const PreferenceStore = require('../stores/preference_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); + +const Constants = require('../utils/constants.jsx'); +const TutorialSteps = Constants.TutorialSteps; +const Preferences = Constants.Preferences; export default class CenterPanel extends React.Component { constructor(props) { super(props); + + this.onPreferenceChange = this.onPreferenceChange.bind(this); + + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + this.state = {showTutorialScreens: parseInt(tutorialPref.value, 10) === TutorialSteps.INTRO_SCREENS}; + } + componentDidMount() { + PreferenceStore.addChangeListener(this.onPreferenceChange); + } + componentWillUnmount() { + PreferenceStore.removeChangeListener(this.onPreferenceChange); + } + onPreferenceChange() { + const tutorialPref = PreferenceStore.getPreference(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), {value: '0'}); + this.setState({showTutorialScreens: parseInt(tutorialPref.value, 10) <= TutorialSteps.INTRO_SCREENS}); } render() { + let postsContainer; + if (this.state.showTutorialScreens) { + postsContainer = <TutorialIntroScreens />; + } else { + postsContainer = <PostsViewContainer />; + } + return ( <div className='inner__wrap channel__wrap'> <div className='row header'> @@ -32,7 +62,7 @@ export default class CenterPanel extends React.Component { <ChannelHeader /> </div> <div id='post-list'> - <PostsViewContainer /> + {postsContainer} </div> <div className='post-create__container' diff --git a/web/react/components/channel_view.jsx b/web/react/components/channel_view.jsx index beafa7d63..3f53a94c2 100644 --- a/web/react/components/channel_view.jsx +++ b/web/react/components/channel_view.jsx @@ -1,10 +1,10 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var CenterPanel = require('../components/center_panel.jsx'); -var Sidebar = require('../components/sidebar.jsx'); -var SidebarRight = require('../components/sidebar_right.jsx'); -var SidebarRightMenu = require('../components/sidebar_right_menu.jsx'); +const CenterPanel = require('../components/center_panel.jsx'); +const Sidebar = require('../components/sidebar.jsx'); +const SidebarRight = require('../components/sidebar_right.jsx'); +const SidebarRightMenu = require('../components/sidebar_right_menu.jsx'); export default class ChannelView extends React.Component { constructor(props) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 7c601af4b..1545cdfaa 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>{'Sending Messages'}</h4> + <p>{'Type here to write a message.'}</p> + <p>{'Click the attachment button to upload an image or a file.'}</p> + </div> + ); + + return ( + <TutorialTip + placement='top' + screens={screens} + overlayClass='tip-overlay--chat' + /> + ); + } 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/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index dc21fad21..f43bdffdf 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -104,7 +104,7 @@ export default class NavbarDropdown extends React.Component { </li> ); - if (this.props.teamType === 'O') { + if (this.props.teamType === Constants.OPEN_TEAM) { teamLink = ( <li> <a diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 9eda2a158..7671ca01d 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -2,15 +2,18 @@ // See License.txt for license information. const PostsView = require('./posts_view.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); + const ChannelStore = require('../stores/channel_store.jsx'); const PostStore = require('../stores/post_store.jsx'); -const Constants = require('../utils/constants.jsx'); -const ActionTypes = Constants.ActionTypes; + const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); const AsyncClient = require('../utils/async_client.jsx'); -const LoadingScreen = require('./loading_screen.jsx'); + +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; import {createChannelIntroMessage} from '../utils/channel_intro_mssages.jsx'; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 023955e97..c47919885 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,51 @@ export default class Sidebar extends React.Component { this.setState({showDirectChannelsModal: false}); } + createTutorialTip() { + const screens = []; + + screens.push( + <div> + <h4>{'Channels'}</h4> + <p><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.'} + </p> + </div> + ); + + screens.push( + <div> + <h4>{'"Town Square" and "Off-Topic" channels'}</h4> + <p>{'Here are two public channels to start:'}</p> + <p> + <strong>{'Town Square'}</strong>{' is a place for team-wide communication. Everyone in your team is a member of this channel.'} + </p> + <p> + <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.'} + </p> + </div> + ); + + screens.push( + <div> + <h4>{'Creating and Joining Channels'}</h4> + <p> + {'Click '}<strong>{'"More..."'}</strong>{' to create a new channel or join an existing one.'} + </p> + <p> + {'You can also create a new channel or private group by clicking the '}<strong>{'"+" symbol'}</strong>{' next to the channel or private group header.'} + </p> + </div> + ); + + return ( + <TutorialTip + placement='right' + screens={screens} + overlayClass='tip-overlay--sidebar' + /> + ); + } + createChannelElement(channel, index, arr, handleClose) { var members = this.state.members; var activeId = this.state.activeId; @@ -444,6 +499,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 +520,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..3f777d93c 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(); @@ -24,6 +46,38 @@ export default class SidebarHeader extends React.Component { } $('.team__header').find('.dropdown-toggle').dropdown('toggle'); } + createTutorialTip() { + const screens = []; + + screens.push( + <div> + <h4>{'Main Menu'}</h4> + <p> + {'The '}<strong>{'Main Menu'}</strong>{' is where you can '} + <strong>{'Invite New Members'}</strong> + {', access your '} + <strong>{'Account Settings'}</strong> + {' and set your '}<strong>{'Theme Color'}</strong>{'.'} + </p> + <p> + {'Team administrators can also access their '}<strong>{'Team Settings'}</strong>{' from this menu.'} + </p> + </div> + ); + + return ( + <div + onClick={this.toggleDropdown} + > + <TutorialTip + ref='tip' + placement='right' + screens={screens} + overlayClass='tip-overlay--header' + /> + </div> + ); + } render() { var me = UserStore.getCurrentUser(); var profilePicture = null; @@ -41,8 +95,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 new file mode 100644 index 000000000..c7abccae3 --- /dev/null +++ b/web/react/components/tutorial/tutorial_intro_screens.jsx @@ -0,0 +1,152 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const UserStore = require('../../stores/user_store.jsx'); +const ChannelStore = require('../../stores/channel_store.jsx'); +const TeamStore = require('../../stores/team_store.jsx'); +const PreferenceStore = require('../../stores/preference_store.jsx'); +const Utils = require('../../utils/utils.jsx'); +const AsyncClient = require('../../utils/async_client.jsx'); + +const Constants = require('../../utils/constants.jsx'); +const Preferences = Constants.Preferences; + +export default class TutorialIntroScreens extends React.Component { + constructor(props) { + super(props); + + this.handleNext = this.handleNext.bind(this); + this.createScreen = this.createScreen.bind(this); + + this.state = {currentScreen: 0}; + } + handleNext() { + if (this.state.currentScreen < 2) { + this.setState({currentScreen: this.state.currentScreen + 1}); + return; + } + + Utils.switchChannel(ChannelStore.getByName(Constants.DEFAULT_CHANNEL)); + + 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.currentScreen) { + case 0: + return this.createScreenOne(); + case 1: + return this.createScreenTwo(); + case 2: + return this.createScreenThree(); + } + } + createScreenOne() { + return ( + <div> + <h3>{'Welcome to:'}</h3> + <h1>{'Mattermost'}</h1> + <p>{'Your team communications all in one place, instantly searchable and available anywhere.'}</p> + <p>{'Keep your team connected to help them achieve what matters most.'}</p> + <div className='tutorial__circles'> + <div className='circle active'/> + <div className='circle'/> + <div className='circle'/> + </div> + </div> + ); + } + createScreenTwo() { + return ( + <div> + <h3>{'How Mattermost works:'}</h3> + <p>{'Communication happens in public discussion channels, private groups and direct messages.'}</p> + <p>{'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}</p> + <div className='tutorial__circles'> + <div className='circle'/> + <div className='circle active'/> + <div className='circle'/> + </div> + </div> + ); + } + createScreenThree() { + const team = TeamStore.getCurrent(); + let inviteModalLink; + if (team.type === Constants.INVITE_TEAM) { + inviteModalLink = ( + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#invite_member' + > + {'Invite teammates'} + </a> + ); + } else { + inviteModalLink = ( + <a + className='intro-links' + href='#' + data-toggle='modal' + data-target='#get_link' + data-title='Team Invite' + data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + team.id} + > + {'Invite teammates'} + </a> + ); + } + + return ( + <div> + <h3>{'You’re all set'}</h3> + <p> + {inviteModalLink} + {' when you’re ready.'} + </p> + <p> + {'Need anything, just email us at '} + <a + href='mailto:feedback@mattermost.com' + target='_blank' + > + {'feedback@mattermost.com'} + </a> + {'.'} + </p> + {'Click “Next” to enter Town Square. This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'} + <div className='tutorial__circles'> + <div className='circle'/> + <div className='circle'/> + <div className='circle active'/> + </div> + </div> + ); + } + render() { + const screen = this.createScreen(); + + return ( + <div className='tutorial-steps__container'> + <div className='tutorial__content'> + <div className='tutorial__steps'> + {screen} + <button + className='btn btn-primary' + tabIndex='1' + onClick={this.handleNext} + > + {'Next'} + </button> + </div> + </div> + </div> + ); + } +} diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx new file mode 100644 index 000000000..c85acb346 --- /dev/null +++ b/web/react/components/tutorial/tutorial_tip.jsx @@ -0,0 +1,131 @@ +// 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( + <div + className='circle active' + key={'dotactive' + i} + /> + ); + } else { + dots.push( + <div + className='circle' + key={'dotinactive' + i} + /> + ); + } + } + } + + return ( + <div className={'tip-div ' + this.props.overlayClass}> + <img + className='tip-button' + src='/static/images/tutorialTip.gif' + width='35' + onClick={this.toggle} + ref='target' + /> + + <Overlay + show={this.state.show} + > + <div className='tip-backdrop'/> + </Overlay> + + <Overlay + placement={this.props.placement} + show={this.state.show} + rootClose={true} + onHide={this.toggle} + target={() => this.refs.target} + > + <div className={'tip-overlay ' + this.props.overlayClass}> + <div className='arrow'></div> + {this.props.screens[this.state.currentScreen]} + <div className='tutorial__circles'>{dots}</div> + <div className='text-right'> + <button + className='btn btn-default' + onClick={this.handleNext} + > + {buttonText} + </button> + <div className='tip-opt'> + {'Seen this before? '} + <a + href='#' + onClick={this.skipTutorial} + > + {'Opt out of these tips.'} + </a> + </div> + </div> + </div> + </Overlay> + </div> + ); + } +} + +TutorialTip.defaultProps = { + overlayClass: '' +}; + +TutorialTip.propTypes = { + screens: React.PropTypes.array.isRequired, + placement: React.PropTypes.string.isRequired, + overlayClass: React.PropTypes.string +}; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 8884d1d10..fd64b1554 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -314,7 +314,14 @@ module.exports = { Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', CATEGORY_DISPLAY_SETTINGS: 'display_settings', - CATEGORY_ADVANCED_SETTINGS: 'advanced_settings' + 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/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 8f353c401..a49a98952 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -179,6 +179,15 @@ } @media screen and (max-width: 1140px) { + .tip-overlay { + &.tip-overlay--chat { + margin: -10px 0 0 -10px; + .arrow { + right: 15px; + left: auto; + } + } + } .inner__wrap { &.move--left { .file-overlay { @@ -266,6 +275,18 @@ } } @media screen and (max-width: 768px) { + .tip-div { + left: 15px; + right: auto; + } + .tip-overlay { + &.tip-overlay--chat { + margin-left: 10px; + .arrow { + left: 32px; + } + } + } .file-details__container { display: block; .file-details__preview { @@ -482,7 +503,6 @@ padding-bottom: 10px; display: table; width: 100%; - table-layout: fixed; .post-body__cell { display: table-cell; padding-left: 45px; diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss new file mode 100644 index 000000000..42183d599 --- /dev/null +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -0,0 +1,188 @@ +.tip-backdrop { + background: rgba(black, 0.5); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; +} + +.tip-overlay { + width: 350px; + max-width: 90%; + position:absolute; + background-color: #fff; + @include border-radius(3px); + padding: 20px; + z-index: 1000; + + .arrow { + border-width: 10px; + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } + + &.tip-overlay--sidebar { + max-width: 75%; + margin: 50px 0 0 10px; + .arrow { + top: 80px; + left: -10px; + margin-top: -10px; + border-left-width: 0; + border-right-color: #fff; + } + } + + &.tip-overlay--header { + margin: 10px 0 0 10px; + .arrow { + top: 15px; + left: -10px; + border-left-width: 0; + border-right-color: #fff; + } + } + + &.tip-overlay--chat { + margin-top: -10px; + .arrow { + left: 50%; + margin-left: -10px; + border-bottom-width: 0; + border-top-color: #fff; + bottom: -10px; + } + } + + h4 { + font-size: em(16px); + font-weight: 600; + margin: 5px 0 15px; + } + + p { + font-size: 13px; + line-height: 1.6; + + strong { + font-weight: 600; + } + + } + + .btn { + background: #cccccc; + color: #fff; + @include border-radius(3px); + border: none; + margin-bottom: 10px; + + &:hover, &:active, &:focus { + color: #fff; + border: none; + background: #bbb; + } + + } + + .tip-opt { + font-size: 12px; + + span { + @include opacity(0.5); + } + + } + + .tutorial__circles { + margin: 1.5em 0px -1.7em -4px; + + .circle { + width: 7px; + height: 7px; + margin: 0 4px; + &.active { + background: #000; + @include opacity(0.4); + } + + } + + } + +} + +.tip-button { + z-index: 998; + right: -10px; + top: -10px; + position: relative; + cursor: pointer; +} + +.tip-div { + position:absolute; + top:0px; + right:0px; + + &.tip-overlay--header { + top: 20px; + } + + &.tip-overlay--sidebar { + left: 0; + top: -9px; + } + +} + +.tutorial-steps__container { + text-align: center; + width: 100%; + display: table; + .tutorial__content { + display: table-cell; + vertical-align: middle; + padding-bottom: 100px; + padding: 0 40px; + .tutorial__steps { + max-width: 310px; + min-height: 420px; + text-align: left; + display: inline-block; + } + } + h1 { + font-size: em(40px); + margin: -20px 0 30px; + font-weight: 600; + } + h3 { + font-size: em(24px); + margin-bottom: 30px; + font-weight: 600; + } +} + +.tutorial__circles { + margin: 2em 0; + .circle { + width: 9px; + height: 9px; + @include border-radius(9px); + @include opacity(0.1); + background: #000; + display: inline-block; + margin: 0 5px; + &.active { + background: $primary-color; + @include opacity(1); + } + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index ad2cae194..5c83d5c5b 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -38,6 +38,7 @@ @import "partials/loading"; @import "partials/get-link"; @import "partials/markdown"; +@import "partials/tutorial"; @import "partials/statistics"; // Responsive Css diff --git a/web/static/images/tutorialTip.gif b/web/static/images/tutorialTip.gif Binary files differnew file mode 100644 index 000000000..f185ff4b9 --- /dev/null +++ b/web/static/images/tutorialTip.gif |