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.

205 lines
6.8 KiB

  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3. import PropTypes from 'prop-types';
  4. import ImmutablePropTypes from 'react-immutable-proptypes';
  5. import Column from '../../components/column';
  6. import ColumnHeader from '../../components/column_header';
  7. import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
  8. import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
  9. import NotificationContainer from './containers/notification_container';
  10. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  11. import ColumnSettingsContainer from './containers/column_settings_container';
  12. import FilterBarContainer from './containers/filter_bar_container';
  13. import { createSelector } from 'reselect';
  14. import { List as ImmutableList } from 'immutable';
  15. import { debounce } from 'lodash';
  16. import ScrollableList from '../../components/scrollable_list';
  17. import LoadGap from '../../components/load_gap';
  18. const messages = defineMessages({
  19. title: { id: 'column.notifications', defaultMessage: 'Notifications' },
  20. });
  21. const getNotifications = createSelector([
  22. state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
  23. state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
  24. state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
  25. state => state.getIn(['notifications', 'items']),
  26. ], (showFilterBar, allowedType, excludedTypes, notifications) => {
  27. if (!showFilterBar || allowedType === 'all') {
  28. // used if user changed the notification settings after loading the notifications from the server
  29. // otherwise a list of notifications will come pre-filtered from the backend
  30. // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
  31. return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
  32. }
  33. return notifications.filter(item => item !== null && allowedType === item.get('type'));
  34. });
  35. const mapStateToProps = state => ({
  36. showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
  37. notifications: getNotifications(state),
  38. isLoading: state.getIn(['notifications', 'isLoading'], true),
  39. isUnread: state.getIn(['notifications', 'unread']) > 0,
  40. hasMore: state.getIn(['notifications', 'hasMore']),
  41. });
  42. export default @connect(mapStateToProps)
  43. @injectIntl
  44. class Notifications extends React.PureComponent {
  45. static propTypes = {
  46. columnId: PropTypes.string,
  47. notifications: ImmutablePropTypes.list.isRequired,
  48. showFilterBar: PropTypes.bool.isRequired,
  49. dispatch: PropTypes.func.isRequired,
  50. shouldUpdateScroll: PropTypes.func,
  51. intl: PropTypes.object.isRequired,
  52. isLoading: PropTypes.bool,
  53. isUnread: PropTypes.bool,
  54. multiColumn: PropTypes.bool,
  55. hasMore: PropTypes.bool,
  56. };
  57. static defaultProps = {
  58. trackScroll: true,
  59. };
  60. componentWillUnmount () {
  61. this.handleLoadOlder.cancel();
  62. this.handleScrollToTop.cancel();
  63. this.handleScroll.cancel();
  64. this.props.dispatch(scrollTopNotifications(false));
  65. }
  66. handleLoadGap = (maxId) => {
  67. this.props.dispatch(expandNotifications({ maxId }));
  68. };
  69. handleLoadOlder = debounce(() => {
  70. const last = this.props.notifications.last();
  71. this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
  72. }, 300, { leading: true });
  73. handleScrollToTop = debounce(() => {
  74. this.props.dispatch(scrollTopNotifications(true));
  75. }, 100);
  76. handleScroll = debounce(() => {
  77. this.props.dispatch(scrollTopNotifications(false));
  78. }, 100);
  79. handlePin = () => {
  80. const { columnId, dispatch } = this.props;
  81. if (columnId) {
  82. dispatch(removeColumn(columnId));
  83. } else {
  84. dispatch(addColumn('NOTIFICATIONS', {}));
  85. }
  86. }
  87. handleMove = (dir) => {
  88. const { columnId, dispatch } = this.props;
  89. dispatch(moveColumn(columnId, dir));
  90. }
  91. handleHeaderClick = () => {
  92. this.column.scrollTop();
  93. }
  94. setColumnRef = c => {
  95. this.column = c;
  96. }
  97. handleMoveUp = id => {
  98. const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
  99. this._selectChild(elementIndex);
  100. }
  101. handleMoveDown = id => {
  102. const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
  103. this._selectChild(elementIndex);
  104. }
  105. _selectChild (index) {
  106. const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
  107. if (element) {
  108. element.focus();
  109. }
  110. }
  111. render () {
  112. const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
  113. const pinned = !!columnId;
  114. const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
  115. let scrollableContent = null;
  116. const filterBarContainer = showFilterBar
  117. ? (<FilterBarContainer />)
  118. : null;
  119. if (isLoading && this.scrollableContent) {
  120. scrollableContent = this.scrollableContent;
  121. } else if (notifications.size > 0 || hasMore) {
  122. scrollableContent = notifications.map((item, index) => item === null ? (
  123. <LoadGap
  124. key={'gap:' + notifications.getIn([index + 1, 'id'])}
  125. disabled={isLoading}
  126. maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
  127. onClick={this.handleLoadGap}
  128. />
  129. ) : (
  130. <NotificationContainer
  131. key={item.get('id')}
  132. notification={item}
  133. accountId={item.get('account')}
  134. onMoveUp={this.handleMoveUp}
  135. onMoveDown={this.handleMoveDown}
  136. />
  137. ));
  138. } else {
  139. scrollableContent = null;
  140. }
  141. this.scrollableContent = scrollableContent;
  142. const scrollContainer = (
  143. <ScrollableList
  144. scrollKey={`notifications-${columnId}`}
  145. trackScroll={!pinned}
  146. isLoading={isLoading}
  147. showLoading={isLoading && notifications.size === 0}
  148. hasMore={hasMore}
  149. emptyMessage={emptyMessage}
  150. onLoadMore={this.handleLoadOlder}
  151. onScrollToTop={this.handleScrollToTop}
  152. onScroll={this.handleScroll}
  153. shouldUpdateScroll={shouldUpdateScroll}
  154. >
  155. {scrollableContent}
  156. </ScrollableList>
  157. );
  158. return (
  159. <Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
  160. <ColumnHeader
  161. icon='bell'
  162. active={isUnread}
  163. title={intl.formatMessage(messages.title)}
  164. onPin={this.handlePin}
  165. onMove={this.handleMove}
  166. onClick={this.handleHeaderClick}
  167. pinned={pinned}
  168. multiColumn={multiColumn}
  169. >
  170. <ColumnSettingsContainer />
  171. </ColumnHeader>
  172. {filterBarContainer}
  173. {scrollContainer}
  174. </Column>
  175. );
  176. }
  177. }