--- /dev/null
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import ReactSwipeableViews from 'react-swipeable-views';
+import classNames from 'classnames';
+import Permalink from '../../../components/permalink';
+import ComposeForm from '../../compose/components/compose_form';
+import Search from '../../compose/components/search';
+import NavigationBar from '../../compose/components/navigation_bar';
+import ColumnHeader from './column_header';
+import { List as ImmutableList } from 'immutable';
+import { me } from '../../../initial_state';
+
+const noop = () => { };
+
+const messages = defineMessages({
+ home_title: { id: 'column.home', defaultMessage: 'Home' },
+ notifications_title: { id: 'column.notifications', defaultMessage: 'Notifications' },
+ local_title: { id: 'column.community', defaultMessage: 'Local timeline' },
+ federated_title: { id: 'column.public', defaultMessage: 'Federated timeline' },
+});
+
+const PageOne = ({ acct, domain }) => (
+ <div className='onboarding-modal__page onboarding-modal__page-one'>
+ <div className='onboarding-modal__page-one__lead'>
+ <h1><FormattedMessage id='onboarding.page_one.welcome' defaultMessage='Welcome to Mastodon!' /></h1>
+ <p><FormattedMessage id='onboarding.page_one.federation' defaultMessage='Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.' /></p>
+ </div>
+
+ <div className='onboarding-modal__page-one__extra'>
+ <div className='display-case'>
+ <div className='display-case__label'>
+ <FormattedMessage id='onboarding.page_one.full_handle' defaultMessage='Your full handle' />
+ </div>
+
+ <div className='display-case__case'>
+ @{acct}@{domain}
+ </div>
+ </div>
+
+ <p><FormattedMessage id='onboarding.page_one.handle_hint' defaultMessage='This is what you would tell your friends to search for.' /></p>
+ </div>
+ </div>
+);
+
+PageOne.propTypes = {
+ acct: PropTypes.string.isRequired,
+ domain: PropTypes.string.isRequired,
+};
+
+const PageTwo = ({ myAccount }) => (
+ <div className='onboarding-modal__page onboarding-modal__page-two'>
+ <div className='figure non-interactive'>
+ <div className='pseudo-drawer'>
+ <NavigationBar account={myAccount} />
+
+ <ComposeForm
+ text='Awoo! #introductions'
+ suggestions={ImmutableList()}
+ mentionedDomains={[]}
+ spoiler={false}
+ onChange={noop}
+ onSubmit={noop}
+ onPaste={noop}
+ onPickEmoji={noop}
+ onChangeSpoilerText={noop}
+ onClearSuggestions={noop}
+ onFetchSuggestions={noop}
+ onSuggestionSelected={noop}
+ showSearch
+ />
+ </div>
+ </div>
+
+ <p><FormattedMessage id='onboarding.page_two.compose' defaultMessage='Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.' /></p>
+ </div>
+);
+
+PageTwo.propTypes = {
+ myAccount: ImmutablePropTypes.map.isRequired,
+};
+
+const PageThree = ({ myAccount }) => (
+ <div className='onboarding-modal__page onboarding-modal__page-three'>
+ <div className='figure non-interactive'>
+ <Search
+ value=''
+ onChange={noop}
+ onSubmit={noop}
+ onClear={noop}
+ onShow={noop}
+ />
+
+ <div className='pseudo-drawer'>
+ <NavigationBar account={myAccount} />
+ </div>
+ </div>
+
+ <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p>
+ <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p>
+ </div>
+);
+
+PageThree.propTypes = {
+ myAccount: ImmutablePropTypes.map.isRequired,
+};
+
+const PageFour = ({ domain, intl }) => (
+ <div className='onboarding-modal__page onboarding-modal__page-four'>
+ <div className='onboarding-modal__page-four__columns'>
+ <div className='row'>
+ <div>
+ <div className='figure non-interactive'><ColumnHeader icon='home' type={intl.formatMessage(messages.home_title)} /></div>
+ <p><FormattedMessage id='onboarding.page_four.home' defaultMessage='The home timeline shows posts from people you follow.' /></p>
+ </div>
+
+ <div>
+ <div className='figure non-interactive'><ColumnHeader icon='bell' type={intl.formatMessage(messages.notifications_title)} /></div>
+ <p><FormattedMessage id='onboarding.page_four.notifications' defaultMessage='The notifications column shows when someone interacts with you.' /></p>
+ </div>
+ </div>
+
+ <div className='row'>
+ <div>
+ <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='users' type={intl.formatMessage(messages.local_title)} /></div>
+ </div>
+
+ <div>
+ <div className='figure non-interactive' style={{ marginBottom: 0 }}><ColumnHeader icon='globe' type={intl.formatMessage(messages.federated_title)} /></div>
+ </div>
+ </div>
+
+ <p><FormattedMessage id='onboarding.page_five.public_timelines' defaultMessage='The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.' values={{ domain }} /></p>
+ </div>
+ </div>
+);
+
+PageFour.propTypes = {
+ domain: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+};
+
+const PageSix = ({ admin, domain }) => {
+ let adminSection = '';
+
+ if (admin) {
+ adminSection = (
+ <p>
+ <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} />
+ <br />
+ <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }} />
+ </p>
+ );
+ }
+
+ return (
+ <div className='onboarding-modal__page onboarding-modal__page-six'>
+ <h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
+ {adminSection}
+ <p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
+ <p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p>
+ <p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p>
+ </div>
+ );
+};
+
+PageSix.propTypes = {
+ admin: ImmutablePropTypes.map,
+ domain: PropTypes.string.isRequired,
+};
+
+const mapStateToProps = state => ({
+ myAccount: state.getIn(['accounts', me]),
+ admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
+ domain: state.getIn(['meta', 'domain']),
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class OnboardingModal extends React.PureComponent {
+
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ myAccount: ImmutablePropTypes.map.isRequired,
+ domain: PropTypes.string.isRequired,
+ admin: ImmutablePropTypes.map,
+ };
+
+ state = {
+ currentIndex: 0,
+ };
+
+ componentWillMount() {
+ const { myAccount, admin, domain, intl } = this.props;
+ this.pages = [
+ <PageOne acct={myAccount.get('acct')} domain={domain} />,
+ <PageTwo myAccount={myAccount} />,
+ <PageThree myAccount={myAccount} />,
+ <PageFour domain={domain} intl={intl} />,
+ <PageSix admin={admin} domain={domain} />,
+ ];
+ };
+
+ componentDidMount() {
+ window.addEventListener('keyup', this.handleKeyUp);
+ }
+
+ componentWillUnmount() {
+ window.addEventListener('keyup', this.handleKeyUp);
+ }
+
+ handleSkip = (e) => {
+ e.preventDefault();
+ this.props.onClose();
+ }
+
+ 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 });
+ }
+
+ handleKeyUp = ({ key }) => {
+ switch (key) {
+ case 'ArrowLeft':
+ this.handlePrev();
+ break;
+ case 'ArrowRight':
+ this.handleNext();
+ break;
+ }
+ }
+
+ handleClose = () => {
+ this.props.onClose();
+ }
+
+ render () {
+ const { pages } = this;
+ const { currentIndex } = this.state;
+ const hasMore = currentIndex < pages.length - 1;
+
+ const nextOrDoneBtn = hasMore ? (
+ <button onClick={this.handleNext} className='onboarding-modal__nav onboarding-modal__next shake-bottom'>
+ <FormattedMessage id='onboarding.next' defaultMessage='Next' /> <i className='fa fa-fw fa-chevron-right' />
+ </button>
+ ) : (
+ <button onClick={this.handleClose} className='onboarding-modal__nav onboarding-modal__done shake-bottom'>
+ <FormattedMessage id='onboarding.done' defaultMessage='Done' /> <i className='fa fa-fw fa-check' />
+ </button>
+ );
+
+ return (
+ <div className='modal-root__modal onboarding-modal'>
+ <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
+ {pages.map((page, i) => {
+ const className = classNames('onboarding-modal__page__wrapper', `onboarding-modal__page__wrapper-${i}`, {
+ 'onboarding-modal__page__wrapper--active': i === currentIndex,
+ });
+
+ return (
+ <div key={i} className={className}>{page}</div>
+ );
+ })}
+ </ReactSwipeableViews>
+
+ <div className='onboarding-modal__paginator'>
+ <div>
+ <button
+ onClick={this.handleSkip}
+ className='onboarding-modal__nav onboarding-modal__skip'
+ >
+ <FormattedMessage id='onboarding.skip' defaultMessage='Skip' />
+ </button>
+ </div>
+
+ <div className='onboarding-modal__dots'>
+ {pages.map((_, i) => {
+ const className = classNames('onboarding-modal__dot', {
+ active: i === currentIndex,
+ });
+
+ return (
+ <div
+ key={`dot-${i}`}
+ role='button'
+ tabIndex='0'
+ data-index={i}
+ onClick={this.handleDot}
+ className={className}
+ />
+ );
+ })}
+ </div>
+
+ <div>
+ {nextOrDoneBtn}
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
color: $primary-text-color;
}
+ a {
+ color: inherit;
+ }
+
.permalink {
text-decoration: none;
}
flex: 1 1 auto;
align-items: center;
justify-content: center;
+
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
}
}
-.pulse-loading {
+.no-reduce-motion .pulse-loading {
transform-origin: center center;
animation: heartbeat 1.5s ease-in-out infinite both;
}
+@keyframes shake-bottom {
+ 0%,
+ 100% {
+ transform: rotate(0deg);
+ transform-origin: 50% 100%;
+ }
+
+ 10% {
+ transform: rotate(2deg);
+ }
+
+ 20%,
+ 40%,
+ 60% {
+ transform: rotate(-4deg);
+ }
+
+ 30%,
+ 50%,
+ 70% {
+ transform: rotate(4deg);
+ }
+
+ 80% {
+ transform: rotate(-2deg);
+ }
+
+ 90% {
+ transform: rotate(2deg);
+ }
+}
+
+.no-reduce-motion .shake-bottom {
+ transform-origin: 50% 100%;
+ animation: shake-bottom 0.8s cubic-bezier(0.455, 0.030, 0.515, 0.955) 2s 2 both;
+}
+
.emoji-picker-dropdown__menu {
background: $simple-background-color;
position: absolute;
z-index: 100;
}
+.onboarding-modal,
.error-modal,
.embed-modal {
background: $ui-secondary-color;
flex-direction: column;
}
+.onboarding-modal__pager {
+ height: 80vh;
+ width: 80vw;
+ max-width: 520px;
+ max-height: 470px;
+
+ .react-swipeable-view-container > div {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ user-select: text;
+ }
+}
+
.error-modal__body {
height: 80vh;
width: 80vw;
text-align: center;
}
+@media screen and (max-width: 550px) {
+ .onboarding-modal {
+ width: 100%;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ .onboarding-modal__pager {
+ width: 100%;
+ height: auto;
+ max-width: none;
+ max-height: none;
+ flex: 1 1 auto;
+ }
+}
+
+.onboarding-modal__paginator,
.error-modal__footer {
flex: 0 0 auto;
background: darken($ui-secondary-color, 8%);
min-width: 33px;
}
+ .onboarding-modal__nav,
.error-modal__nav {
color: darken($ui-secondary-color, 34%);
- background-color: transparent;
border: 0;
font-size: 14px;
font-weight: 500;
- padding: 0;
+ padding: 10px 25px;
line-height: inherit;
height: auto;
+ margin: -10px;
+ border-radius: 4px;
+ background-color: transparent;
&:hover,
&:focus,
&:active {
color: darken($ui-secondary-color, 38%);
+ background-color: darken($ui-secondary-color, 16%);
+ }
+
+ &.onboarding-modal__done,
+ &.onboarding-modal__next {
+ color: $ui-base-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: darken($ui-base-color, 4%);
+ }
}
}
}
justify-content: center;
}
+.onboarding-modal__dots {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.onboarding-modal__dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 14px;
+ background: darken($ui-secondary-color, 16%);
+ margin: 0 3px;
+ cursor: pointer;
+
+ &:hover {
+ background: darken($ui-secondary-color, 18%);
+ }
+
+ &.active {
+ cursor: default;
+ background: darken($ui-secondary-color, 24%);
+ }
+}
+
+.onboarding-modal__page__wrapper {
+ pointer-events: none;
+ padding: 25px;
+ padding-bottom: 0;
+
+ &.onboarding-modal__page__wrapper--active {
+ pointer-events: auto;
+ }
+}
+
+.onboarding-modal__page {
+ cursor: default;
+ line-height: 21px;
+
+ h1 {
+ font-size: 18px;
+ font-weight: 500;
+ color: $ui-base-color;
+ margin-bottom: 20px;
+ }
+
+ a {
+ color: $ui-highlight-color;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: lighten($ui-highlight-color, 4%);
+ }
+ }
+
+ .navigation-bar a {
+ color: inherit;
+ }
+
+ p {
+ font-size: 16px;
+ color: lighten($ui-base-color, 8%);
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ strong {
+ font-weight: 500;
+ background: $ui-base-color;
+ color: $ui-secondary-color;
+ border-radius: 4px;
+ font-size: 14px;
+ padding: 3px 6px;
+
+ @each $lang in $cjk-langs {
+ &:lang(#{$lang}) {
+ font-weight: 700;
+ }
+ }
+ }
+ }
+}
+
+.onboarding-modal__page__wrapper-0 {
+ background: url('../images/elephant_ui_greeting.svg') no-repeat left bottom / auto 250px;
+ height: 100%;
+ padding: 0;
+}
+
+.onboarding-modal__page-one {
+ &__lead {
+ padding: 65px;
+ padding-top: 45px;
+ padding-bottom: 0;
+ margin-bottom: 10px;
+
+ h1 {
+ font-size: 26px;
+ line-height: 36px;
+ margin-bottom: 8px;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+ }
+
+ &__extra {
+ padding-right: 65px;
+ padding-left: 185px;
+ text-align: center;
+ }
+}
+
+.display-case {
+ text-align: center;
+ font-size: 15px;
+ margin-bottom: 15px;
+
+ &__label {
+ font-weight: 500;
+ color: $ui-base-color;
+ margin-bottom: 5px;
+ text-transform: uppercase;
+ font-size: 12px;
+ }
+
+ &__case {
+ background: $ui-base-color;
+ color: $ui-secondary-color;
+ font-weight: 500;
+ padding: 10px;
+ border-radius: 4px;
+ }
+}
+
+.onboarding-modal__page-two,
+.onboarding-modal__page-three,
+.onboarding-modal__page-four,
+.onboarding-modal__page-five {
+ p {
+ text-align: left;
+ }
+
+ .figure {
+ background: darken($ui-base-color, 8%);
+ color: $ui-secondary-color;
+ margin-bottom: 20px;
+ border-radius: 4px;
+ padding: 10px;
+ text-align: center;
+ font-size: 14px;
+ box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.3);
+
+ .onboarding-modal__image {
+ border-radius: 4px;
+ margin-bottom: 10px;
+ }
+
+ &.non-interactive {
+ pointer-events: none;
+ text-align: left;
+ }
+ }
+}
+
+.onboarding-modal__page-four__columns {
+ .row {
+ display: flex;
+ margin-bottom: 20px;
+
+ & > div {
+ flex: 1 1 0;
+ margin: 0 10px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+
+ p {
+ text-align: center;
+ }
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .column-header {
+ color: $primary-text-color;
+ }
+}
+
+@media screen and (max-width: 320px) and (max-height: 600px) {
+ .onboarding-modal__page p {
+ font-size: 14px;
+ line-height: 20px;
+ }
+
+ .onboarding-modal__page-two .figure,
+ .onboarding-modal__page-three .figure,
+ .onboarding-modal__page-four .figure,
+ .onboarding-modal__page-five .figure {
+ font-size: 12px;
+ margin-bottom: 10px;
+ }
+
+ .onboarding-modal__page-four__columns .row {
+ margin-bottom: 10px;
+ }
+
+ .onboarding-modal__page-four__columns .column-header {
+ padding: 5px;
+ font-size: 12px;
+ }
+}
+
+.onboard-sliders {
+ display: inline-block;
+ max-width: 30px;
+ max-height: auto;
+ margin-left: 10px;
+}
+
.boost-modal,
.confirmation-modal,
.report-modal,