diff --git a/app/javascript/images/elephant_ui_greeting.svg b/app/javascript/images/elephant_ui_greeting.svg
new file mode 100644
index 00000000000..f3eb4b142a1
--- /dev/null
+++ b/app/javascript/images/elephant_ui_greeting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js
new file mode 100644
index 00000000000..a161c50efed
--- /dev/null
+++ b/app/javascript/mastodon/actions/onboarding.js
@@ -0,0 +1,14 @@
+import { openModal } from './modal';
+import { changeSetting, saveSettings } from './settings';
+
+export function showOnboardingOnce() {
+ return (dispatch, getState) => {
+ const alreadySeen = getState().getIn(['settings', 'onboarded']);
+
+ if (!alreadySeen) {
+ dispatch(openModal('ONBOARDING'));
+ dispatch(changeSetting(['onboarded'], true));
+ dispatch(saveSettings());
+ }
+ };
+};
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 8ae3b727af9..d1710445b53 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -2,6 +2,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import configureStore from '../store/configureStore';
+import { showOnboardingOnce } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
@@ -39,6 +40,8 @@ export default class Mastodon extends React.PureComponent {
const handlerUrl = window.location.protocol + '//' + window.location.host + '/intent?uri=%s';
window.setTimeout(() => navigator.registerProtocolHandler('web+mastodon', handlerUrl, 'Mastodon'), 5 * 60 * 1000);
}
+
+ store.dispatch(showOnboardingOnce());
}
componentWillUnmount () {
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index dbfb46ee747..5839ba40a64 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -9,6 +9,7 @@ import VideoModal from './video_modal';
import BoostModal from './boost_modal';
import ConfirmationModal from './confirmation_modal';
import {
+ OnboardingModal,
MuteModal,
ReportModal,
EmbedModal,
@@ -17,6 +18,7 @@ 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 }),
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
new file mode 100644
index 00000000000..9b713cf9ee5
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -0,0 +1,324 @@
+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 }) => (
+
+
+
+
+
+
+
+
+
+
+ @{acct}@{domain}
+
+
+
+
+
+
+);
+
+PageOne.propTypes = {
+ acct: PropTypes.string.isRequired,
+ domain: PropTypes.string.isRequired,
+};
+
+const PageTwo = ({ myAccount }) => (
+
+);
+
+PageTwo.propTypes = {
+ myAccount: ImmutablePropTypes.map.isRequired,
+};
+
+const PageThree = ({ myAccount }) => (
+
+
+
+
#illustration, introductions: #introductions }} />
+
+
+);
+
+PageThree.propTypes = {
+ myAccount: ImmutablePropTypes.map.isRequired,
+};
+
+const PageFour = ({ domain, intl }) => (
+
+);
+
+PageFour.propTypes = {
+ domain: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+};
+
+const PageSix = ({ admin, domain }) => {
+ let adminSection = '';
+
+ if (admin) {
+ adminSection = (
+
+ @{admin.get('acct')} }} />
+
+ }} />
+
+ );
+ }
+
+ return (
+
+
+ {adminSection}
+
GitHub }} />
+
}} />
+
+
+ );
+};
+
+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 = [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ];
+ };
+
+ 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 ? (
+
+ ) : (
+
+ );
+
+ return (
+
+
+ {pages.map((page, i) => {
+ const className = classNames('onboarding-modal__page__wrapper', `onboarding-modal__page__wrapper-${i}`, {
+ 'onboarding-modal__page__wrapper--active': i === currentIndex,
+ });
+
+ return (
+ {page}
+ );
+ })}
+
+
+
+
+
+
+
+
+ {pages.map((_, i) => {
+ const className = classNames('onboarding-modal__dot', {
+ active: i === currentIndex,
+ });
+
+ return (
+
+ );
+ })}
+
+
+
+ {nextOrDoneBtn}
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index a03c4cefd8e..d6586680b8f 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -94,6 +94,10 @@ export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
}
+export function OnboardingModal () {
+ return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
+}
+
export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d214fe85fe5..8315763bfbf 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -174,7 +174,7 @@
"onboarding.page_four.home": "The home timeline shows posts from people you follow.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
- "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
+ "onboarding.page_one.handle": "Your full handle is {handle}. This is what you would tell your friends to search for.",
"onboarding.page_one.welcome": "Welcome to Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.",
"onboarding.page_six.almost_done": "Almost done...",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c0a32ed053e..4a9a379a8e2 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1419,6 +1419,10 @@
color: $primary-text-color;
}
+ a {
+ color: inherit;
+ }
+
.permalink {
text-decoration: none;
}
@@ -2760,6 +2764,7 @@
flex: 1 1 auto;
align-items: center;
justify-content: center;
+
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
}
@@ -2805,11 +2810,48 @@
}
}
-.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;
@@ -3300,6 +3342,7 @@
z-index: 100;
}
+.onboarding-modal,
.error-modal,
.embed-modal {
background: $ui-secondary-color;
@@ -3310,6 +3353,25 @@
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;
@@ -3343,6 +3405,23 @@
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%);
@@ -3353,20 +3432,35 @@
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%);
+ }
}
}
}
@@ -3375,6 +3469,239 @@
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,