Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new first-time tutorial #9531

Merged
merged 5 commits into from
Dec 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/javascript/images/screen_federation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/javascript/images/screen_hello.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/javascript/images/screen_interactions.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 4 additions & 10 deletions app/javascript/mastodon/actions/onboarding.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { openModal } from './modal';
import { changeSetting, saveSettings } from './settings';

export function showOnboardingOnce() {
return (dispatch, getState) => {
const alreadySeen = getState().getIn(['settings', 'onboarded']);
export const INTRODUCTION_VERSION = 20181216044202;

if (!alreadySeen) {
dispatch(openModal('ONBOARDING'));
dispatch(changeSetting(['onboarded'], true));
dispatch(saveSettings());
}
};
export const closeOnboarding = () => dispatch => {
dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION));
dispatch(saveSettings());
};
51 changes: 34 additions & 17 deletions app/javascript/mastodon/containers/mastodon.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Provider, connect } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
import { showOnboardingOnce } from '../actions/onboarding';
import { INTRODUCTION_VERSION } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
Expand All @@ -18,11 +19,39 @@ addLocaleData(localeData);

export const store = configureStore();
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);

// load custom emojis
store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis());

const mapStateToProps = state => ({
showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
});

@connect(mapStateToProps)
class MastodonMount extends React.PureComponent {

static propTypes = {
showIntroduction: PropTypes.bool,
};

render () {
const { showIntroduction } = this.props;

if (showIntroduction) {
return <Introduction />;
}

return (
<BrowserRouter basename='/web'>
<ScrollContext>
<Route path='/' component={UI} />
</ScrollContext>
</BrowserRouter>
);
}

}

export default class Mastodon extends React.PureComponent {

static propTypes = {
Expand All @@ -31,14 +60,6 @@ export default class Mastodon extends React.PureComponent {

componentDidMount() {
this.disconnect = store.dispatch(connectUserStream());

// Desktop notifications
// Ask after 1 minute
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
window.setTimeout(() => Notification.requestPermission(), 60 * 1000);
}

store.dispatch(showOnboardingOnce());
}

componentWillUnmount () {
Expand All @@ -54,11 +75,7 @@ export default class Mastodon extends React.PureComponent {
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<BrowserRouter basename='/web'>
<ScrollContext>
<Route path='/' component={UI} />
</ScrollContext>
</BrowserRouter>
<MastodonMount />
</Provider>
</IntlProvider>
);
Expand Down
196 changes: 196 additions & 0 deletions app/javascript/mastodon/features/introduction/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactSwipeableViews from 'react-swipeable-views';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { closeOnboarding } from '../../actions/onboarding';
import screenHello from '../../../images/screen_hello.svg';
import screenFederation from '../../../images/screen_federation.svg';
import screenInteractions from '../../../images/screen_interactions.svg';
import logoTransparent from '../../../images/logo_transparent.svg';

const FrameWelcome = ({ domain, onNext }) => (
<div className='introduction__frame'>
<div className='introduction__illustration' style={{ background: `url(${logoTransparent}) no-repeat center center / auto 80%` }}>
<img src={screenHello} alt='' />
</div>

<div className='introduction__text introduction__text--centered'>
<h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3>
<p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p>
</div>

<div className='introduction__action'>
<button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button>
</div>
</div>
);

FrameWelcome.propTypes = {
domain: PropTypes.string.isRequired,
onNext: PropTypes.func.isRequired,
};

const FrameFederation = ({ onNext }) => (
<div className='introduction__frame'>
<div className='introduction__illustration'>
<img src={screenFederation} alt='' />
</div>

<div className='introduction__text introduction__text--columnized'>
<div>
<h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3>
<p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p>
</div>

<div>
<h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3>
<p><FormattedMessage id='introduction.federation.local.text' defaultMessage='Public posts from people on the same server as you will appear in the local timeline.' /></p>
</div>

<div>
<h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3>
<p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p>
</div>
</div>

<div className='introduction__action'>
<button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button>
</div>
</div>
);

FrameFederation.propTypes = {
onNext: PropTypes.func.isRequired,
};

const FrameInteractions = ({ onNext }) => (
<div className='introduction__frame'>
<div className='introduction__illustration'>
<img src={screenInteractions} alt='' />
</div>

<div className='introduction__text introduction__text--columnized'>
<div>
<h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3>
<p><FormattedMessage id='introduction.interactions.reply.text' defaultMessage="You can reply to other people's and your own toots, which will chain them together in a conversation." /></p>
</div>

<div>
<h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Boost' /></h3>
<p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's toots with your followers by boosting them." /></p>
</div>

<div>
<h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favourite' /></h3>
<p><FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a toot for later, and let the author know that you liked it, by favouriting it.' /></p>
</div>
</div>

<div className='introduction__action'>
<button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button>
</div>
</div>
);

FrameInteractions.propTypes = {
onNext: PropTypes.func.isRequired,
};

@connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
export default class Introduction extends React.PureComponent {

static propTypes = {
domain: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired,
};

state = {
currentIndex: 0,
};

componentWillMount () {
this.pages = [
<FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
<FrameFederation onNext={this.handleNext} />,
<FrameInteractions onNext={this.handleFinish} />,
];
}

componentDidMount() {
window.addEventListener('keyup', this.handleKeyUp);
}

componentWillUnmount() {
window.addEventListener('keyup', this.handleKeyUp);
}

handleDot = (e) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.setState({ currentIndex: i });
}

handlePrev = () => {
this.setState(({ currentIndex }) => ({
currentIndex: Math.max(0, currentIndex - 1),
}));
}

handleNext = () => {
const { pages } = this;

this.setState(({ currentIndex }) => ({
currentIndex: Math.min(currentIndex + 1, pages.length - 1),
}));
}

handleSwipe = (index) => {
this.setState({ currentIndex: index });
}

handleFinish = () => {
this.props.dispatch(closeOnboarding());
}

handleKeyUp = ({ key }) => {
switch (key) {
case 'ArrowLeft':
this.handlePrev();
break;
case 'ArrowRight':
this.handleNext();
break;
}
}

render () {
const { currentIndex } = this.state;
const { pages } = this;

return (
<div className='introduction'>
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
{pages.map((page, i) => (
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
))}
</ReactSwipeableViews>

<div className='introduction__dots'>
{pages.map((_, i) => (
<div
key={`dot-${i}`}
role='button'
tabIndex='0'
data-index={i}
onClick={this.handleDot}
className={classNames('introduction__dot', { active: i === currentIndex })}
/>
))}
</div>
</div>
);
}

}
2 changes: 0 additions & 2 deletions app/javascript/mastodon/features/ui/components/modal_root.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal';
import {
OnboardingModal,
MuteModal,
ReportModal,
EmbedModal,
Expand All @@ -21,7 +20,6 @@ import {

const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
'ONBOARDING': OnboardingModal,
'VIDEO': () => Promise.resolve({ default: VideoModal }),
'BOOST': () => Promise.resolve({ default: BoostModal }),
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
Expand Down
Loading