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.

236 lines
7.9 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { defineMessages, injectIntl } from 'react-intl';
  4. import ImmutablePropTypes from 'react-immutable-proptypes';
  5. import ImmutablePureComponent from 'react-immutable-pure-component';
  6. import ReactSwipeableViews from 'react-swipeable-views';
  7. import TabsBar, { links, getIndex, getLink } from './tabs_bar';
  8. import { Link } from 'react-router-dom';
  9. import BundleContainer from '../containers/bundle_container';
  10. import ColumnLoading from './column_loading';
  11. import DrawerLoading from './drawer_loading';
  12. import BundleColumnError from './bundle_column_error';
  13. import {
  14. Compose,
  15. Notifications,
  16. HomeTimeline,
  17. CommunityTimeline,
  18. PublicTimeline,
  19. HashtagTimeline,
  20. DirectTimeline,
  21. FavouritedStatuses,
  22. BookmarkedStatuses,
  23. ListTimeline,
  24. Directory,
  25. } from '../../ui/util/async-components';
  26. import Icon from 'mastodon/components/icon';
  27. import ComposePanel from './compose_panel';
  28. import NavigationPanel from './navigation_panel';
  29. import detectPassiveEvents from 'detect-passive-events';
  30. import { scrollRight } from '../../../scroll';
  31. const componentMap = {
  32. 'COMPOSE': Compose,
  33. 'HOME': HomeTimeline,
  34. 'NOTIFICATIONS': Notifications,
  35. 'PUBLIC': PublicTimeline,
  36. 'COMMUNITY': CommunityTimeline,
  37. 'HASHTAG': HashtagTimeline,
  38. 'DIRECT': DirectTimeline,
  39. 'FAVOURITES': FavouritedStatuses,
  40. 'BOOKMARKS': BookmarkedStatuses,
  41. 'LIST': ListTimeline,
  42. 'DIRECTORY': Directory,
  43. };
  44. const messages = defineMessages({
  45. publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
  46. });
  47. const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
  48. export default @(component => injectIntl(component, { withRef: true }))
  49. class ColumnsArea extends ImmutablePureComponent {
  50. static contextTypes = {
  51. router: PropTypes.object.isRequired,
  52. };
  53. static propTypes = {
  54. intl: PropTypes.object.isRequired,
  55. columns: ImmutablePropTypes.list.isRequired,
  56. isModalOpen: PropTypes.bool.isRequired,
  57. singleColumn: PropTypes.bool,
  58. children: PropTypes.node,
  59. };
  60. state = {
  61. shouldAnimate: false,
  62. }
  63. componentWillReceiveProps() {
  64. this.setState({ shouldAnimate: false });
  65. }
  66. componentDidMount() {
  67. if (!this.props.singleColumn) {
  68. this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
  69. }
  70. this.lastIndex = getIndex(this.context.router.history.location.pathname);
  71. this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
  72. this.setState({ shouldAnimate: true });
  73. }
  74. componentWillUpdate(nextProps) {
  75. if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
  76. this.node.removeEventListener('wheel', this.handleWheel);
  77. }
  78. }
  79. componentDidUpdate(prevProps) {
  80. if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
  81. this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
  82. }
  83. this.lastIndex = getIndex(this.context.router.history.location.pathname);
  84. this.setState({ shouldAnimate: true });
  85. }
  86. componentWillUnmount () {
  87. if (!this.props.singleColumn) {
  88. this.node.removeEventListener('wheel', this.handleWheel);
  89. }
  90. }
  91. handleChildrenContentChange() {
  92. if (!this.props.singleColumn) {
  93. const modifier = this.isRtlLayout ? -1 : 1;
  94. this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
  95. }
  96. }
  97. handleSwipe = (index) => {
  98. this.pendingIndex = index;
  99. const nextLinkTranslationId = links[index].props['data-preview-title-id'];
  100. const currentLinkSelector = '.tabs-bar__link.active';
  101. const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
  102. // HACK: Remove the active class from the current link and set it to the next one
  103. // React-router does this for us, but too late, feeling laggy.
  104. document.querySelector(currentLinkSelector).classList.remove('active');
  105. document.querySelector(nextLinkSelector).classList.add('active');
  106. if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
  107. this.context.router.history.push(getLink(this.pendingIndex));
  108. this.pendingIndex = null;
  109. }
  110. }
  111. handleAnimationEnd = () => {
  112. if (typeof this.pendingIndex === 'number') {
  113. this.context.router.history.push(getLink(this.pendingIndex));
  114. this.pendingIndex = null;
  115. }
  116. }
  117. handleWheel = () => {
  118. if (typeof this._interruptScrollAnimation !== 'function') {
  119. return;
  120. }
  121. this._interruptScrollAnimation();
  122. }
  123. setRef = (node) => {
  124. this.node = node;
  125. }
  126. renderView = (link, index) => {
  127. const columnIndex = getIndex(this.context.router.history.location.pathname);
  128. const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
  129. const icon = link.props['data-preview-icon'];
  130. const view = (index === columnIndex) ?
  131. React.cloneElement(this.props.children) :
  132. <ColumnLoading title={title} icon={icon} />;
  133. return (
  134. <div className='columns-area columns-area--mobile' key={index}>
  135. {view}
  136. </div>
  137. );
  138. }
  139. renderLoading = columnId => () => {
  140. return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
  141. }
  142. renderError = (props) => {
  143. return <BundleColumnError {...props} />;
  144. }
  145. render () {
  146. const { columns, children, singleColumn, isModalOpen, intl } = this.props;
  147. const { shouldAnimate } = this.state;
  148. const columnIndex = getIndex(this.context.router.history.location.pathname);
  149. if (singleColumn) {
  150. const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
  151. const content = columnIndex !== -1 ? (
  152. <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
  153. {links.map(this.renderView)}
  154. </ReactSwipeableViews>
  155. ) : (
  156. <div key='content' className='columns-area columns-area--mobile'>{children}</div>
  157. );
  158. return (
  159. <div className='columns-area__panels'>
  160. <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
  161. <div className='columns-area__panels__pane__inner'>
  162. <ComposePanel />
  163. </div>
  164. </div>
  165. <div className='columns-area__panels__main'>
  166. <TabsBar key='tabs' />
  167. {content}
  168. </div>
  169. <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
  170. <div className='columns-area__panels__pane__inner'>
  171. <NavigationPanel />
  172. </div>
  173. </div>
  174. {floatingActionButton}
  175. </div>
  176. );
  177. }
  178. return (
  179. <div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
  180. {columns.map(column => {
  181. const params = column.get('params', null) === null ? null : column.get('params').toJS();
  182. const other = params && params.other ? params.other : {};
  183. return (
  184. <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
  185. {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
  186. </BundleContainer>
  187. );
  188. })}
  189. {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
  190. </div>
  191. );
  192. }
  193. }