diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx index b871fe81ae..242c2c6379 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 = ; + } else { + postsContainer = ; + } + return (
@@ -32,7 +62,7 @@ export default class CenterPanel extends React.Component {
- + {postsContainer}
+

{'Sending Messages'}

+

{'Type here to write a message.'}

+

{'Click the attachment button to upload an image or a file.'}

+
+ ); + + return ( + + ); + } 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 (
+ {tutorialTip}
{postError} diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index dc21fad21e..f43bdffdfc 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 { ); - if (this.props.teamType === 'O') { + if (this.props.teamType === Constants.OPEN_TEAM) { teamLink = (
  • +

    {'Channels'}

    +

    {'Channels'}{' organize conversations across different topics. They’re open to everyone on your team. To send private communications use '}{'Direct Messages'}{' for a single person or '}{'Private Groups'}{' for multiple people.'} +

    +
  • + ); + + screens.push( +
    +

    {'"Town Square" and "Off-Topic" channels'}

    +

    {'Here are two public channels to start:'}

    +

    + {'Town Square'}{' is a place for team-wide communication. Everyone in your team is a member of this channel.'} +

    +

    + {'Off-Topic'}{' is a place for fun and humor outside of work-related channels. You and your team can decide what other channels to create.'} +

    +
    + ); + + screens.push( +
    +

    {'Creating and Joining Channels'}

    +

    + {'Click '}{'"More..."'}{' to create a new channel or join an existing one.'} +

    +

    + {'You can also create a new channel or private group by clicking the '}{'"+" symbol'}{' next to the channel or private group header.'} +

    +
    + ); + + return ( + + ); + } + 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 (
  • + {tutorialTip}
  • ); } diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 65e4c6d7eb..3f777d93c0 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( +
    +

    {'Main Menu'}

    +

    + {'The '}{'Main Menu'}{' is where you can '} + {'Invite New Members'} + {', access your '} + {'Account Settings'} + {' and set your '}{'Theme Color'}{'.'} +

    +

    + {'Team administrators can also access their '}{'Team Settings'}{' from this menu.'} +

    +
    + ); + + return ( +
    + +
    + ); + } 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 (
    + {tutorialTip} +

    {'Welcome to:'}

    +

    {'Mattermost'}

    +

    {'Your team communications all in one place, instantly searchable and available anywhere.'}

    +

    {'Keep your team connected to help them achieve what matters most.'}

    +
    +
    +
    +
    +
    +
    + ); + } + createScreenTwo() { + return ( +
    +

    {'How Mattermost works:'}

    +

    {'Communication happens in public discussion channels, private groups and direct messages.'}

    +

    {'Everything is archived and searchable from any web-enabled laptop, tablet or phone.'}

    +
    +
    +
    +
    +
    +
    + ); + } + createScreenThree() { + const team = TeamStore.getCurrent(); + let inviteModalLink; + if (team.type === Constants.INVITE_TEAM) { + inviteModalLink = ( +
    + {'Invite teammates'} + + ); + } else { + inviteModalLink = ( + + ); + } + + return ( +
    +

    {'You’re all set'}

    +

    + {inviteModalLink} + {' when you’re ready.'} +

    +

    + {'Need anything, just email us at '} + + {'feedback@mattermost.com'} + + {'.'} +

    + {'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.'} +
    +
    +
    +
    +
    +
    + ); + } + render() { + const screen = this.createScreen(); + + return ( +
    +
    +
    + {screen} + +
    +
    +
    + ); + } +} diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx new file mode 100644 index 0000000000..c85acb3462 --- /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( +
    + ); + } else { + dots.push( +
    + ); + } + } + } + + return ( +
    + + + +
    + + + this.refs.target} + > +
    +
    + {this.props.screens[this.state.currentScreen]} +
    {dots}
    +
    + +
    + {'Seen this before? '} + + {'Opt out of these tips.'} + +
    +
    +
    +
    +
    + ); + } +} + +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 8884d1d10c..fd64b1554a 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 8f353c4019..a49a989522 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 0000000000..42183d5998 --- /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 ad2cae194e..5c83d5c5b4 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 new file mode 100644 index 0000000000..f185ff4b96 Binary files /dev/null and b/web/static/images/tutorialTip.gif differ