Skip to content

Commit

Permalink
Update the first-time introduction
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron committed Dec 16, 2018
1 parent ce77110 commit 343705d
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 595 deletions.
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());
};
24 changes: 9 additions & 15 deletions app/javascript/mastodon/containers/mastodon.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
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';
Expand All @@ -24,11 +24,11 @@ store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis());

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

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

static propTypes = {
showIntroduction: PropTypes.bool,
Expand All @@ -42,9 +42,11 @@ class MastodonMount extends React.Component {
}

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

Expand All @@ -58,12 +60,6 @@ export default class Mastodon extends React.PureComponent {

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

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

componentWillUnmount () {
Expand All @@ -79,9 +75,7 @@ export default class Mastodon extends React.PureComponent {
return (
<IntlProvider locale={locale} messages={messages}>
<Provider store={store}>
<BrowserRouter basename='/web'>
<MastodonMount />
</BrowserRouter>
<MastodonMount />
</Provider>
</IntlProvider>
);
Expand Down
184 changes: 183 additions & 1 deletion app/javascript/mastodon/features/introduction/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,192 @@
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';

const FrameWelcome = ({ onNext }) => (
<div className='introduction__frame'>
<div className='introduction__illustration'>
<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="Since this is your first time, let's go over the basics of Mastodon so you can make it your new home. You will be able to share and exchange messages with your followers all across the fediverse, since Mastodon is decentralized. But this server is special—it hosts your profile, so remember its name." /></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 = {
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()
export default class Introduction extends React.PureComponent {

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

state = {
currentIndex: 0,
};

componentWillMount () {
this.pages = [
<FrameWelcome 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 />
<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>
);
}

Expand Down
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

0 comments on commit 343705d

Please sign in to comment.