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
6.8 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 { 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 { Drawer, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
  14. import detectPassiveEvents from 'detect-passive-events';
  15. import { scrollRight } from 'flavours/glitch/util/scroll';
  16. const componentMap = {
  17. 'COMPOSE': Drawer,
  18. 'HOME': HomeTimeline,
  19. 'NOTIFICATIONS': Notifications,
  20. 'PUBLIC': PublicTimeline,
  21. 'COMMUNITY': CommunityTimeline,
  22. 'HASHTAG': HashtagTimeline,
  23. 'DIRECT': DirectTimeline,
  24. 'FAVOURITES': FavouritedStatuses,
  25. 'BOOKMARKS': BookmarkedStatuses,
  26. 'LIST': ListTimeline,
  27. };
  28. const shouldHideFAB = path => path.match(/^\/statuses\//);
  29. const messages = defineMessages({
  30. publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
  31. });
  32. @component => injectIntl(component, { withRef: true })
  33. export default class ColumnsArea extends ImmutablePureComponent {
  34. static contextTypes = {
  35. router: PropTypes.object.isRequired,
  36. };
  37. static propTypes = {
  38. intl: PropTypes.object.isRequired,
  39. columns: ImmutablePropTypes.list.isRequired,
  40. swipeToChangeColumns: PropTypes.bool,
  41. singleColumn: PropTypes.bool,
  42. children: PropTypes.node,
  43. };
  44. state = {
  45. shouldAnimate: false,
  46. }
  47. componentWillReceiveProps() {
  48. this.setState({ shouldAnimate: false });
  49. }
  50. componentDidMount() {
  51. if (!this.props.singleColumn) {
  52. this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
  53. }
  54. this.lastIndex = getIndex(this.context.router.history.location.pathname);
  55. this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
  56. this.setState({ shouldAnimate: true });
  57. }
  58. componentWillUpdate(nextProps) {
  59. if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
  60. this.node.removeEventListener('wheel', this.handleWheel);
  61. }
  62. }
  63. componentDidUpdate(prevProps) {
  64. if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
  65. this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
  66. }
  67. this.lastIndex = getIndex(this.context.router.history.location.pathname);
  68. this.setState({ shouldAnimate: true });
  69. }
  70. componentWillUnmount () {
  71. if (!this.props.singleColumn) {
  72. this.node.removeEventListener('wheel', this.handleWheel);
  73. }
  74. }
  75. handleChildrenContentChange() {
  76. if (!this.props.singleColumn) {
  77. const modifier = this.isRtlLayout ? -1 : 1;
  78. this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
  79. }
  80. }
  81. handleSwipe = (index) => {
  82. this.pendingIndex = index;
  83. const nextLinkTranslationId = links[index].props['data-preview-title-id'];
  84. const currentLinkSelector = '.tabs-bar__link.active';
  85. const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
  86. // HACK: Remove the active class from the current link and set it to the next one
  87. // React-router does this for us, but too late, feeling laggy.
  88. document.querySelector(currentLinkSelector).classList.remove('active');
  89. document.querySelector(nextLinkSelector).classList.add('active');
  90. }
  91. handleAnimationEnd = () => {
  92. if (typeof this.pendingIndex === 'number') {
  93. this.context.router.history.push(getLink(this.pendingIndex));
  94. this.pendingIndex = null;
  95. }
  96. }
  97. handleWheel = () => {
  98. if (typeof this._interruptScrollAnimation !== 'function') {
  99. return;
  100. }
  101. this._interruptScrollAnimation();
  102. }
  103. setRef = (node) => {
  104. this.node = node;
  105. }
  106. renderView = (link, index) => {
  107. const columnIndex = getIndex(this.context.router.history.location.pathname);
  108. const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
  109. const icon = link.props['data-preview-icon'];
  110. const view = (index === columnIndex) ?
  111. React.cloneElement(this.props.children) :
  112. <ColumnLoading title={title} icon={icon} />;
  113. return (
  114. <div className='columns-area' key={index}>
  115. {view}
  116. </div>
  117. );
  118. }
  119. renderLoading = columnId => () => {
  120. return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
  121. }
  122. renderError = (props) => {
  123. return <BundleColumnError {...props} />;
  124. }
  125. render () {
  126. const { columns, children, singleColumn, swipeToChangeColumns, intl } = this.props;
  127. const { shouldAnimate } = this.state;
  128. const columnIndex = getIndex(this.context.router.history.location.pathname);
  129. this.pendingIndex = null;
  130. if (singleColumn) {
  131. 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)}><i className='fa fa-pencil' /></Link>;
  132. return columnIndex !== -1 ? [
  133. <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={!swipeToChangeColumns}>
  134. {links.map(this.renderView)}
  135. </ReactSwipeableViews>,
  136. floatingActionButton,
  137. ] : [
  138. <div className='columns-area'>{children}</div>,
  139. floatingActionButton,
  140. ];
  141. }
  142. return (
  143. <div className='columns-area' ref={this.setRef}>
  144. {columns.map(column => {
  145. const params = column.get('params', null) === null ? null : column.get('params').toJS();
  146. const other = params && params.other ? params.other : {};
  147. return (
  148. <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
  149. {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
  150. </BundleContainer>
  151. );
  152. })}
  153. {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
  154. </div>
  155. );
  156. }
  157. }