index.jsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import PropTypes from 'prop-types';
  2. import { PureComponent } from 'react';
  3. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  4. import classNames from 'classnames';
  5. import { Helmet } from 'react-helmet';
  6. import { connect } from 'react-redux';
  7. import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
  8. import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
  9. import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/announcements';
  10. import { IconWithBadge } from 'mastodon/components/icon_with_badge';
  11. import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
  12. import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container';
  13. import { criticalUpdatesPending } from 'mastodon/initial_state';
  14. import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
  15. import { expandHomeTimeline } from '../../actions/timelines';
  16. import Column from '../../components/column';
  17. import ColumnHeader from '../../components/column_header';
  18. import StatusListContainer from '../ui/containers/status_list_container';
  19. import { ColumnSettings } from './components/column_settings';
  20. import { CriticalUpdateBanner } from './components/critical_update_banner';
  21. const messages = defineMessages({
  22. title: { id: 'column.home', defaultMessage: 'Home' },
  23. show_announcements: { id: 'home.show_announcements', defaultMessage: 'Show announcements' },
  24. hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
  25. });
  26. const mapStateToProps = state => ({
  27. hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
  28. isPartial: state.getIn(['timelines', 'home', 'isPartial']),
  29. hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
  30. unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
  31. showAnnouncements: state.getIn(['announcements', 'show']),
  32. });
  33. class HomeTimeline extends PureComponent {
  34. static contextTypes = {
  35. identity: PropTypes.object,
  36. };
  37. static propTypes = {
  38. dispatch: PropTypes.func.isRequired,
  39. intl: PropTypes.object.isRequired,
  40. hasUnread: PropTypes.bool,
  41. isPartial: PropTypes.bool,
  42. columnId: PropTypes.string,
  43. multiColumn: PropTypes.bool,
  44. hasAnnouncements: PropTypes.bool,
  45. unreadAnnouncements: PropTypes.number,
  46. showAnnouncements: PropTypes.bool,
  47. };
  48. handlePin = () => {
  49. const { columnId, dispatch } = this.props;
  50. if (columnId) {
  51. dispatch(removeColumn(columnId));
  52. } else {
  53. dispatch(addColumn('HOME', {}));
  54. }
  55. };
  56. handleMove = (dir) => {
  57. const { columnId, dispatch } = this.props;
  58. dispatch(moveColumn(columnId, dir));
  59. };
  60. handleHeaderClick = () => {
  61. this.column.scrollTop();
  62. };
  63. setRef = c => {
  64. this.column = c;
  65. };
  66. handleLoadMore = maxId => {
  67. this.props.dispatch(expandHomeTimeline({ maxId }));
  68. };
  69. componentDidMount () {
  70. setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
  71. this._checkIfReloadNeeded(false, this.props.isPartial);
  72. }
  73. componentDidUpdate (prevProps) {
  74. this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
  75. }
  76. componentWillUnmount () {
  77. this._stopPolling();
  78. }
  79. _checkIfReloadNeeded (wasPartial, isPartial) {
  80. const { dispatch } = this.props;
  81. if (wasPartial === isPartial) {
  82. return;
  83. } else if (!wasPartial && isPartial) {
  84. this.polling = setInterval(() => {
  85. dispatch(expandHomeTimeline());
  86. }, 3000);
  87. } else if (wasPartial && !isPartial) {
  88. this._stopPolling();
  89. }
  90. }
  91. _stopPolling () {
  92. if (this.polling) {
  93. clearInterval(this.polling);
  94. this.polling = null;
  95. }
  96. }
  97. handleToggleAnnouncementsClick = (e) => {
  98. e.stopPropagation();
  99. this.props.dispatch(toggleShowAnnouncements());
  100. };
  101. render () {
  102. const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
  103. const pinned = !!columnId;
  104. const { signedIn } = this.context.identity;
  105. const banners = [];
  106. let announcementsButton;
  107. if (hasAnnouncements) {
  108. announcementsButton = (
  109. <button
  110. type='button'
  111. className={classNames('column-header__button', { 'active': showAnnouncements })}
  112. title={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
  113. aria-label={intl.formatMessage(showAnnouncements ? messages.hide_announcements : messages.show_announcements)}
  114. onClick={this.handleToggleAnnouncementsClick}
  115. >
  116. <IconWithBadge id='bullhorn' icon={CampaignIcon} count={unreadAnnouncements} />
  117. </button>
  118. );
  119. }
  120. if (criticalUpdatesPending) {
  121. banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
  122. }
  123. return (
  124. <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
  125. <ColumnHeader
  126. icon='home'
  127. iconComponent={HomeIcon}
  128. active={hasUnread}
  129. title={intl.formatMessage(messages.title)}
  130. onPin={this.handlePin}
  131. onMove={this.handleMove}
  132. onClick={this.handleHeaderClick}
  133. pinned={pinned}
  134. multiColumn={multiColumn}
  135. extraButton={announcementsButton}
  136. appendContent={hasAnnouncements && showAnnouncements && <AnnouncementsContainer />}
  137. >
  138. <ColumnSettings />
  139. </ColumnHeader>
  140. {signedIn ? (
  141. <StatusListContainer
  142. prepend={banners}
  143. alwaysPrepend
  144. trackScroll={!pinned}
  145. scrollKey={`home_timeline-${columnId}`}
  146. onLoadMore={this.handleLoadMore}
  147. timelineId='home'
  148. emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up.' />}
  149. bindToDocument={!multiColumn}
  150. />
  151. ) : <NotSignedInIndicator />}
  152. <Helmet>
  153. <title>{intl.formatMessage(messages.title)}</title>
  154. <meta name='robots' content='noindex' />
  155. </Helmet>
  156. </Column>
  157. );
  158. }
  159. }
  160. export default connect(mapStateToProps)(injectIntl(HomeTimeline));