From 6d70a8026309c2d41e2402ec84875ced8e181105 Mon Sep 17 00:00:00 2001 From: Eugen Date: Sun, 16 Apr 2017 20:32:00 +0200 Subject: Onboarding modal (#1883) * Basic onboarding modal that's shown to users once * Lay out pages 2 through 5, add images, style modals (#1509) * Lay out pages 2 through 5 Added images and laid out pages 2 through 5 in the jsx file. SCSS will come, still working on just seeing if this works at all. * Fix jsx errors, add images to modal pages, style modal pages * Add animations to onboarding pager changes, improve wording and styling * Finishing touches on the onboarding * Add missing propTypes * Update wording --- .../javascripts/components/actions/onboarding.jsx | 14 ++ .../javascripts/components/containers/mastodon.jsx | 3 + .../features/ui/components/modal_root.jsx | 2 + .../features/ui/components/onboarding_modal.jsx | 251 +++++++++++++++++++++ .../javascripts/components/reducers/settings.jsx | 2 + 5 files changed, 272 insertions(+) create mode 100644 app/assets/javascripts/components/actions/onboarding.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx (limited to 'app/assets/javascripts/components') diff --git a/app/assets/javascripts/components/actions/onboarding.jsx b/app/assets/javascripts/components/actions/onboarding.jsx new file mode 100644 index 000000000..a161c50ef --- /dev/null +++ b/app/assets/javascripts/components/actions/onboarding.jsx @@ -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/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index a771d1269..08576913e 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -8,6 +8,7 @@ import { connectTimeline, disconnectTimeline } from '../actions/timelines'; +import { showOnboardingOnce } from '../actions/onboarding'; import { updateNotifications, refreshNotifications } from '../actions/notifications'; import createBrowserHistory from 'history/lib/createBrowserHistory'; import { @@ -134,6 +135,8 @@ const Mastodon = React.createClass({ if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { Notification.requestPermission(); } + + store.dispatch(showOnboardingOnce()); }, componentWillUnmount () { diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx index a1ed8fd88..ace3e085f 100644 --- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -1,11 +1,13 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import MediaModal from './media_modal'; +import OnboardingModal from './onboarding_modal'; import VideoModal from './video_modal'; import BoostModal from './boost_modal'; import { TransitionMotion, spring } from 'react-motion'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, + 'ONBOARDING': OnboardingModal, 'VIDEO': VideoModal, 'BOOST': BoostModal }; diff --git a/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx new file mode 100644 index 000000000..8d5132ea2 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx @@ -0,0 +1,251 @@ +import { connect } from 'react-redux'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Permalink from '../../../components/permalink'; +import { TransitionMotion, spring } from 'react-motion'; +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 Immutable from 'immutable'; + +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: React.PropTypes.string.isRequired, + domain: React.PropTypes.string.isRequired +}; + +const PageTwo = () => ( +
+
+ {}} + onSubmit={() => {}} + onPaste={() => {}} + onPickEmoji={() => {}} + onChangeSpoilerText={() => {}} + onClearSuggestions={() => {}} + onFetchSuggestions={() => {}} + onSuggestionSelected={() => {}} + /> +
+ +

+
+); + +const PageThree = ({ me, domain }) => ( +
+
+ {}} + onSubmit={() => {}} + onClear={() => {}} + onShow={() => {}} + /> + +
+ +
+
+ +

#illustration, introductions: #introductions }}/>

+

+
+); + +PageThree.propTypes = { + me: ImmutablePropTypes.map.isRequired, + domain: React.PropTypes.string.isRequired +}; + +const PageFour = ({ domain, intl }) => ( +
+
+
+
+
+

+
+ +
+
+

+
+
+ +
+
+
+
+ +
+
+
+
+ +

+
+
+); + +PageFour.propTypes = { + domain: React.PropTypes.string.isRequired, + intl: React.PropTypes.object.isRequired +}; + +const PageSix = ({ admin }) => { + let adminSection = ''; + + if (admin) { + adminSection = ( +

+ @{admin.get('acct')} }} /> +
+ }}/> +

+ ); + } + + return ( +
+

+ {adminSection} +

GitHub }} />

+

}} />

+
+ ); +}; + +PageSix.propTypes = { + admin: ImmutablePropTypes.map +}; + +const mapStateToProps = state => ({ + me: state.getIn(['accounts', state.getIn(['meta', 'me'])]), + admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), + domain: state.getIn(['meta', 'domain']) +}); + +const OnboardingModal = React.createClass({ + + propTypes: { + onClose: React.PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired, + me: ImmutablePropTypes.map.isRequired, + domain: React.PropTypes.string.isRequired, + admin: ImmutablePropTypes.map + }, + + getInitialState () { + return { + currentIndex: 0 + }; + }, + + mixins: [PureRenderMixin], + + handleSkip (e) { + e.preventDefault(); + this.props.onClose(); + }, + + handleDot (i, e) { + e.preventDefault(); + this.setState({ currentIndex: i }); + }, + + handleNext (maxNum, e) { + e.preventDefault(); + + if (this.state.currentIndex < maxNum - 1) { + this.setState({ currentIndex: this.state.currentIndex + 1 }); + } else { + this.props.onClose(); + } + }, + + render () { + const { me, admin, domain, intl } = this.props; + + const pages = [ + , + , + , + , + + ]; + + const { currentIndex } = this.state; + const hasMore = currentIndex < pages.length - 1; + + let nextOrDoneBtn; + + if(hasMore) { + nextOrDoneBtn = ; + } else { + nextOrDoneBtn = ; + } + + const styles = pages.map((page, i) => ({ + key: i, + style: { opacity: spring(i === currentIndex ? 1 : 0) } + })); + + return ( +
+ + {interpolatedStyles => +
+ {pages.map((page, i) => +
{page}
+ )} +
+ } +
+ +
+
+ +
+ +
+ {pages.map((_, i) =>
)} +
+ +
+ {nextOrDoneBtn} +
+
+
+ ); + } + +}); + +export default connect(mapStateToProps)(injectIntl(OnboardingModal)); diff --git a/app/assets/javascripts/components/reducers/settings.jsx b/app/assets/javascripts/components/reducers/settings.jsx index 8acc3faca..820af99ed 100644 --- a/app/assets/javascripts/components/reducers/settings.jsx +++ b/app/assets/javascripts/components/reducers/settings.jsx @@ -3,6 +3,8 @@ import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ + onboarded: false, + home: Immutable.Map({ shows: Immutable.Map({ reblog: true, -- cgit