summaryrefslogtreecommitdiffstats
path: root/web/react
diff options
context:
space:
mode:
authorCorey Hulen <corey@hulen.com>2015-11-02 14:01:53 -0800
committerCorey Hulen <corey@hulen.com>2015-11-02 14:01:53 -0800
commit1086297336b05af6861e05b2023df6044284aba6 (patch)
treed3d95d78e99e9368393dae4f0e4ade3b6750cadc /web/react
parent6360ce37da25029a4f62b14df8e01a62f64820a8 (diff)
parent6b1e80d1d55783016d295f3f116891ce65ae6b06 (diff)
downloadchat-1086297336b05af6861e05b2023df6044284aba6.tar.gz
chat-1086297336b05af6861e05b2023df6044284aba6.tar.bz2
chat-1086297336b05af6861e05b2023df6044284aba6.zip
Merge pull request #1269 from mattermost/plt-848-3
PLT-848 Add tutorial intro screens and popover tips
Diffstat (limited to 'web/react')
-rw-r--r--web/react/components/center_panel.jsx42
-rw-r--r--web/react/components/channel_view.jsx8
-rw-r--r--web/react/components/create_post.jsx62
-rw-r--r--web/react/components/navbar_dropdown.jsx2
-rw-r--r--web/react/components/posts_view_container.jsx9
-rw-r--r--web/react/components/sidebar.jsx75
-rw-r--r--web/react/components/sidebar_header.jsx66
-rw-r--r--web/react/components/tutorial/tutorial_intro_screens.jsx152
-rw-r--r--web/react/components/tutorial/tutorial_tip.jsx131
-rw-r--r--web/react/utils/constants.jsx9
10 files changed, 518 insertions, 38 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,