You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
7.0 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ReactSwipeableViews from 'react-swipeable-views';
  4. import classNames from 'classnames';
  5. import { connect } from 'react-redux';
  6. import { FormattedMessage } from 'react-intl';
  7. import { closeOnboarding } from '../../actions/onboarding';
  8. import screenHello from '../../../images/screen_hello.svg';
  9. import screenFederation from '../../../images/screen_federation.svg';
  10. import screenInteractions from '../../../images/screen_interactions.svg';
  11. import logoTransparent from '../../../images/logo_transparent.svg';
  12. import { disableSwiping } from 'mastodon/initial_state';
  13. const FrameWelcome = ({ domain, onNext }) => (
  14. <div className='introduction__frame'>
  15. <div className='introduction__illustration' style={{ background: `url(${logoTransparent}) no-repeat center center / auto 80%` }}>
  16. <img src={screenHello} alt='' />
  17. </div>
  18. <div className='introduction__text introduction__text--centered'>
  19. <h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3>
  20. <p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p>
  21. </div>
  22. <div className='introduction__action'>
  23. <button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button>
  24. </div>
  25. </div>
  26. );
  27. FrameWelcome.propTypes = {
  28. domain: PropTypes.string.isRequired,
  29. onNext: PropTypes.func.isRequired,
  30. };
  31. const FrameFederation = ({ onNext }) => (
  32. <div className='introduction__frame'>
  33. <div className='introduction__illustration'>
  34. <img src={screenFederation} alt='' />
  35. </div>
  36. <div className='introduction__text introduction__text--columnized'>
  37. <div>
  38. <h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3>
  39. <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>
  40. </div>
  41. <div>
  42. <h3><FormattedMessage id='introduction.federation.local.headline' defaultMessage='Local' /></h3>
  43. <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>
  44. </div>
  45. <div>
  46. <h3><FormattedMessage id='introduction.federation.federated.headline' defaultMessage='Federated' /></h3>
  47. <p><FormattedMessage id='introduction.federation.federated.text' defaultMessage='Public posts from other servers of the fediverse will appear in the federated timeline.' /></p>
  48. </div>
  49. </div>
  50. <div className='introduction__action'>
  51. <button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button>
  52. </div>
  53. </div>
  54. );
  55. FrameFederation.propTypes = {
  56. onNext: PropTypes.func.isRequired,
  57. };
  58. const FrameInteractions = ({ onNext }) => (
  59. <div className='introduction__frame'>
  60. <div className='introduction__illustration'>
  61. <img src={screenInteractions} alt='' />
  62. </div>
  63. <div className='introduction__text introduction__text--columnized'>
  64. <div>
  65. <h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3>
  66. <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>
  67. </div>
  68. <div>
  69. <h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Boost' /></h3>
  70. <p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's toots with your followers by boosting them." /></p>
  71. </div>
  72. <div>
  73. <h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favourite' /></h3>
  74. <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>
  75. </div>
  76. </div>
  77. <div className='introduction__action'>
  78. <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button>
  79. </div>
  80. </div>
  81. );
  82. FrameInteractions.propTypes = {
  83. onNext: PropTypes.func.isRequired,
  84. };
  85. export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
  86. class Introduction extends React.PureComponent {
  87. static propTypes = {
  88. domain: PropTypes.string.isRequired,
  89. dispatch: PropTypes.func.isRequired,
  90. };
  91. state = {
  92. currentIndex: 0,
  93. };
  94. componentWillMount () {
  95. this.pages = [
  96. <FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
  97. <FrameFederation onNext={this.handleNext} />,
  98. <FrameInteractions onNext={this.handleFinish} />,
  99. ];
  100. }
  101. componentDidMount() {
  102. window.addEventListener('keyup', this.handleKeyUp);
  103. }
  104. componentWillUnmount() {
  105. window.addEventListener('keyup', this.handleKeyUp);
  106. }
  107. handleDot = (e) => {
  108. const i = Number(e.currentTarget.getAttribute('data-index'));
  109. e.preventDefault();
  110. this.setState({ currentIndex: i });
  111. }
  112. handlePrev = () => {
  113. this.setState(({ currentIndex }) => ({
  114. currentIndex: Math.max(0, currentIndex - 1),
  115. }));
  116. }
  117. handleNext = () => {
  118. const { pages } = this;
  119. this.setState(({ currentIndex }) => ({
  120. currentIndex: Math.min(currentIndex + 1, pages.length - 1),
  121. }));
  122. }
  123. handleSwipe = (index) => {
  124. this.setState({ currentIndex: index });
  125. }
  126. handleFinish = () => {
  127. this.props.dispatch(closeOnboarding());
  128. }
  129. handleKeyUp = ({ key }) => {
  130. switch (key) {
  131. case 'ArrowLeft':
  132. this.handlePrev();
  133. break;
  134. case 'ArrowRight':
  135. this.handleNext();
  136. break;
  137. }
  138. }
  139. render () {
  140. const { currentIndex } = this.state;
  141. const { pages } = this;
  142. return (
  143. <div className='introduction'>
  144. <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} disabled={disableSwiping} className='introduction__pager'>
  145. {pages.map((page, i) => (
  146. <div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
  147. ))}
  148. </ReactSwipeableViews>
  149. <div className='introduction__dots'>
  150. {pages.map((_, i) => (
  151. <div
  152. key={`dot-${i}`}
  153. role='button'
  154. tabIndex='0'
  155. data-index={i}
  156. onClick={this.handleDot}
  157. className={classNames('introduction__dot', { active: i === currentIndex })}
  158. />
  159. ))}
  160. </div>
  161. </div>
  162. );
  163. }
  164. }