columns_area.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  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 { Link } from 'react-router-dom';
  7. import BundleContainer from '../containers/bundle_container';
  8. import ColumnLoading from './column_loading';
  9. import DrawerLoading from './drawer_loading';
  10. import BundleColumnError from './bundle_column_error';
  11. import {
  12. Compose,
  13. Notifications,
  14. HomeTimeline,
  15. CommunityTimeline,
  16. PublicTimeline,
  17. HashtagTimeline,
  18. DirectTimeline,
  19. FavouritedStatuses,
  20. BookmarkedStatuses,
  21. ListTimeline,
  22. Directory,
  23. } from '../../ui/util/async-components';
  24. import Icon from 'mastodon/components/icon';
  25. import ComposePanel from './compose_panel';
  26. import NavigationPanel from './navigation_panel';
  27. import { supportsPassiveEvents } from 'detect-passive-events';
  28. import { scrollRight } from '../../../scroll';
  29. const componentMap = {
  30. 'COMPOSE': Compose,
  31. 'HOME': HomeTimeline,
  32. 'NOTIFICATIONS': Notifications,
  33. 'PUBLIC': PublicTimeline,
  34. 'REMOTE': PublicTimeline,
  35. 'COMMUNITY': CommunityTimeline,
  36. 'HASHTAG': HashtagTimeline,
  37. 'DIRECT': DirectTimeline,
  38. 'FAVOURITES': FavouritedStatuses,
  39. 'BOOKMARKS': BookmarkedStatuses,
  40. 'LIST': ListTimeline,
  41. 'DIRECTORY': Directory,
  42. };
  43. const messages = defineMessages({
  44. publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
  45. });
  46. const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/explore|^\/getting-started|^\/start/);
  47. export default @(component => injectIntl(component, { withRef: true }))
  48. class ColumnsArea extends ImmutablePureComponent {
  49. static contextTypes = {
  50. router: PropTypes.object.isRequired,
  51. identity: 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. // Corresponds to (max-width: $no-gap-breakpoint + 285px - 1px) in SCSS
  61. mediaQuery = 'matchMedia' in window && window.matchMedia('(max-width: 1174px)');
  62. state = {
  63. renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
  64. }
  65. componentDidMount() {
  66. if (!this.props.singleColumn) {
  67. this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
  68. }
  69. if (this.mediaQuery) {
  70. if (this.mediaQuery.addEventListener) {
  71. this.mediaQuery.addEventListener('change', this.handleLayoutChange);
  72. } else {
  73. this.mediaQuery.addListener(this.handleLayoutChange);
  74. }
  75. this.setState({ renderComposePanel: !this.mediaQuery.matches });
  76. }
  77. this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
  78. }
  79. componentWillUpdate(nextProps) {
  80. if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
  81. this.node.removeEventListener('wheel', this.handleWheel);
  82. }
  83. }
  84. componentDidUpdate(prevProps) {
  85. if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
  86. this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false);
  87. }
  88. }
  89. componentWillUnmount () {
  90. if (!this.props.singleColumn) {
  91. this.node.removeEventListener('wheel', this.handleWheel);
  92. }
  93. if (this.mediaQuery) {
  94. if (this.mediaQuery.removeEventListener) {
  95. this.mediaQuery.removeEventListener('change', this.handleLayoutChange);
  96. } else {
  97. this.mediaQuery.removeListener(this.handleLayouteChange);
  98. }
  99. }
  100. }
  101. handleChildrenContentChange() {
  102. if (!this.props.singleColumn) {
  103. const modifier = this.isRtlLayout ? -1 : 1;
  104. this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
  105. }
  106. }
  107. handleLayoutChange = (e) => {
  108. this.setState({ renderComposePanel: !e.matches });
  109. }
  110. handleWheel = () => {
  111. if (typeof this._interruptScrollAnimation !== 'function') {
  112. return;
  113. }
  114. this._interruptScrollAnimation();
  115. }
  116. setRef = (node) => {
  117. this.node = node;
  118. }
  119. renderLoading = columnId => () => {
  120. return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
  121. }
  122. renderError = (props) => {
  123. return <BundleColumnError multiColumn {...props} />;
  124. }
  125. render () {
  126. const { columns, children, singleColumn, isModalOpen, intl } = this.props;
  127. const { renderComposePanel } = this.state;
  128. const { signedIn } = this.context.identity;
  129. if (singleColumn) {
  130. const floatingActionButton = (!signedIn || shouldHideFAB(this.context.router.history.location.pathname)) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
  131. return (
  132. <div className='columns-area__panels'>
  133. <div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
  134. <div className='columns-area__panels__pane__inner'>
  135. {renderComposePanel && <ComposePanel />}
  136. </div>
  137. </div>
  138. <div className={`columns-area__panels__main ${floatingActionButton && 'with-fab'}`}>
  139. <div className='tabs-bar__wrapper'><div id='tabs-bar__portal' /></div>
  140. <div className='columns-area columns-area--mobile'>{children}</div>
  141. </div>
  142. <div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
  143. <div className='columns-area__panels__pane__inner'>
  144. <NavigationPanel />
  145. </div>
  146. </div>
  147. {floatingActionButton}
  148. </div>
  149. );
  150. }
  151. return (
  152. <div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
  153. {columns.map(column => {
  154. const params = column.get('params', null) === null ? null : column.get('params').toJS();
  155. const other = params && params.other ? params.other : {};
  156. return (
  157. <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
  158. {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
  159. </BundleContainer>
  160. );
  161. })}
  162. {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
  163. </div>
  164. );
  165. }
  166. }