From 6433e039adf3e876a3836647e8e8e307370b3abf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 3 Apr 2017 18:27:09 +0200 Subject: [PATCH] Basic onboarding modal that's shown to users once --- .../components/actions/onboarding.jsx | 14 +++ .../components/containers/mastodon.jsx | 3 + .../features/ui/components/modal_root.jsx | 4 +- .../ui/components/onboarding_modal.jsx | 116 ++++++++++++++++++ .../components/reducers/settings.jsx | 2 + app/assets/stylesheets/components.scss | 115 +++++++++++++++++ app/controllers/home_controller.rb | 1 + app/views/home/initial_state.json.rabl | 9 +- 8 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/components/actions/onboarding.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx diff --git a/app/assets/javascripts/components/actions/onboarding.jsx b/app/assets/javascripts/components/actions/onboarding.jsx new file mode 100644 index 00000000..a161c50e --- /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 6dc08bb4..1650eb6a 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 { @@ -106,6 +107,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 d2ae5e14..0d20d7f8 100644 --- a/app/assets/javascripts/components/features/ui/components/modal_root.jsx +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -1,9 +1,11 @@ import PureRenderMixin from 'react-addons-pure-render-mixin'; import MediaModal from './media_modal'; +import OnboardingModal from './onboarding_modal'; import { TransitionMotion, spring } from 'react-motion'; const MODAL_COMPONENTS = { - 'MEDIA': MediaModal + 'MEDIA': MediaModal, + 'ONBOARDING': OnboardingModal }; const ModalRoot = React.createClass({ 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 00000000..5afca860 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/onboarding_modal.jsx @@ -0,0 +1,116 @@ +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'; + +const PageOne = ({ acct, domain }) => ( +
+

+

}} />

+

{domain}, handle: @{acct}@{domain} }}/>

+
+); + +const PageTwo = ({ admin }) => ( +
+

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

+

GitHub }} />

+

}} />

+

+
+); + +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 } = this.props; + + const pages = [ + , + + ]; + + const { currentIndex } = this.state; + const hasMore = currentIndex < pages.length - 1; + + let nextOrDoneBtn; + + if(hasMore) { + nextOrDoneBtn = ; + } else { + nextOrDoneBtn = ; + } + + return ( +
+
+ {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 8acc3fac..820af99e 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, diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index f8003e5f..431959b1 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -1893,3 +1893,118 @@ button.active i.fa-retweet { max-height: 80vh; } } + +.onboarding-modal { + background: $color2; + color: $color1; + border-radius: 8px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.onboarding-modal__pager { + height: 80vh; + width: 80vw; + max-width: 500px; + max-height: 500px; + display: flex; + + & > div { + padding: 25px; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + user-select: text; + + &.active { + display: flex; + } + } +} + +.onboarding-modal__paginator { + flex: 0 0 auto; + background: darken($color2, 8%); + display: flex; + padding: 25px; + + & > div { + min-width: 33px; + } + + a { + color: darken($color2, 34%); + text-decoration: none; + font-size: 14px; + font-weight: 500; + + &:hover, &:focus, &:active { + color: darken($color2, 38%); + } + + &.onboarding-modal__done, &.onboarding-modal__next { + color: $color4; + } + } +} + +.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($color2, 16%); + margin: 0 3px; + cursor: pointer; + + &:hover { + background: darken($color2, 18%); + } + + &.active { + cursor: default; + background: darken($color2, 24%); + } +} + +.onboarding-modal__page { + text-align: center; + cursor: default; + + h1 { + font-size: 18px; + font-weight: 500; + color: $color1; + margin-bottom: 20px; + } + + a { + color: $color4; + + &:hover, &:focus, &:active { + color: lighten($color4, 4%); + } + } + + p { + font-size: 16px; + color: lighten($color1, 8%); + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + strong { + font-weight: 500; + } + } +} diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 814b1f75..0b894af2 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,6 +7,7 @@ class HomeController < ApplicationController @body_classes = 'app-body' @token = find_or_create_access_token.token @web_settings = Web::Setting.find_by(user: current_user)&.data || {} + @admin = Account.find_local(Setting.site_contact_username) end private diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index 71949ab0..bf559039 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -4,7 +4,9 @@ node(:meta) do { access_token: @token, locale: I18n.locale, + domain: Rails.configuration.x.local_domain, me: current_account.id, + admin: @admin.try(:id), } end @@ -16,9 +18,10 @@ node(:compose) do end node(:accounts) do - { - current_account.id => partial('api/v1/accounts/show', object: current_account), - } + store = {} + store[current_account.id] = partial('api/v1/accounts/show', object: current_account) + store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil? + store end node(:settings) { @web_settings }