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.

225 lines
7.7 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, loadPending, mountNotifications, unmountNotifications } 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 || state.getIn(['notifications', 'pendingItems']).size > 0,
  40. hasMore: state.getIn(['notifications', 'hasMore']),
  41. numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
  42. });
  43. export default @connect(mapStateToProps)
  44. @injectIntl
  45. class Notifications extends React.PureComponent {
  46. static propTypes = {
  47. columnId: PropTypes.string,
  48. notifications: ImmutablePropTypes.list.isRequired,
  49. showFilterBar: PropTypes.bool.isRequired,
  50. dispatch: PropTypes.func.isRequired,
  51. shouldUpdateScroll: PropTypes.func,
  52. intl: PropTypes.object.isRequired,
  53. isLoading: PropTypes.bool,
  54. isUnread: PropTypes.bool,
  55. multiColumn: PropTypes.bool,
  56. hasMore: PropTypes.bool,
  57. numPending: PropTypes.number,
  58. };
  59. static defaultProps = {
  60. trackScroll: true,
  61. };
  62. componentWillMount() {
  63. this.props.dispatch(mountNotifications());
  64. }
  65. componentWillUnmount () {
  66. this.handleLoadOlder.cancel();
  67. this.handleScrollToTop.cancel();
  68. this.handleScroll.cancel();
  69. this.props.dispatch(scrollTopNotifications(false));
  70. this.props.dispatch(unmountNotifications());
  71. }
  72. handleLoadGap = (maxId) => {
  73. this.props.dispatch(expandNotifications({ maxId }));
  74. };
  75. handleLoadOlder = debounce(() => {
  76. const last = this.props.notifications.last();
  77. this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
  78. }, 300, { leading: true });
  79. handleLoadPending = () => {
  80. this.props.dispatch(loadPending());
  81. };
  82. handleScrollToTop = debounce(() => {
  83. this.props.dispatch(scrollTopNotifications(true));
  84. }, 100);
  85. handleScroll = debounce(() => {
  86. this.props.dispatch(scrollTopNotifications(false));
  87. }, 100);
  88. handlePin = () => {
  89. const { columnId, dispatch } = this.props;
  90. if (columnId) {
  91. dispatch(removeColumn(columnId));
  92. } else {
  93. dispatch(addColumn('NOTIFICATIONS', {}));
  94. }
  95. }
  96. handleMove = (dir) => {
  97. const { columnId, dispatch } = this.props;
  98. dispatch(moveColumn(columnId, dir));
  99. }
  100. handleHeaderClick = () => {
  101. this.column.scrollTop();
  102. }
  103. setColumnRef = c => {
  104. this.column = c;
  105. }
  106. handleMoveUp = id => {
  107. const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
  108. this._selectChild(elementIndex, true);
  109. }
  110. handleMoveDown = id => {
  111. const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
  112. this._selectChild(elementIndex, false);
  113. }
  114. _selectChild (index, align_top) {
  115. const container = this.column.node;
  116. const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
  117. if (element) {
  118. if (align_top && container.scrollTop > element.offsetTop) {
  119. element.scrollIntoView(true);
  120. } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
  121. element.scrollIntoView(false);
  122. }
  123. element.focus();
  124. }
  125. }
  126. render () {
  127. const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
  128. const pinned = !!columnId;
  129. const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
  130. let scrollableContent = null;
  131. const filterBarContainer = showFilterBar
  132. ? (<FilterBarContainer />)
  133. : null;
  134. if (isLoading && this.scrollableContent) {
  135. scrollableContent = this.scrollableContent;
  136. } else if (notifications.size > 0 || hasMore) {
  137. scrollableContent = notifications.map((item, index) => item === null ? (
  138. <LoadGap
  139. key={'gap:' + notifications.getIn([index + 1, 'id'])}
  140. disabled={isLoading}
  141. maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
  142. onClick={this.handleLoadGap}
  143. />
  144. ) : (
  145. <NotificationContainer
  146. key={item.get('id')}
  147. notification={item}
  148. accountId={item.get('account')}
  149. onMoveUp={this.handleMoveUp}
  150. onMoveDown={this.handleMoveDown}
  151. />
  152. ));
  153. } else {
  154. scrollableContent = null;
  155. }
  156. this.scrollableContent = scrollableContent;
  157. const scrollContainer = (
  158. <ScrollableList
  159. scrollKey={`notifications-${columnId}`}
  160. trackScroll={!pinned}
  161. isLoading={isLoading}
  162. showLoading={isLoading && notifications.size === 0}
  163. hasMore={hasMore}
  164. numPending={numPending}
  165. emptyMessage={emptyMessage}
  166. onLoadMore={this.handleLoadOlder}
  167. onLoadPending={this.handleLoadPending}
  168. onScrollToTop={this.handleScrollToTop}
  169. onScroll={this.handleScroll}
  170. shouldUpdateScroll={shouldUpdateScroll}
  171. bindToDocument={!multiColumn}
  172. >
  173. {scrollableContent}
  174. </ScrollableList>
  175. );
  176. return (
  177. <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
  178. <ColumnHeader
  179. icon='bell'
  180. active={isUnread}
  181. title={intl.formatMessage(messages.title)}
  182. onPin={this.handlePin}
  183. onMove={this.handleMove}
  184. onClick={this.handleHeaderClick}
  185. pinned={pinned}
  186. multiColumn={multiColumn}
  187. >
  188. <ColumnSettingsContainer />
  189. </ColumnHeader>
  190. {filterBarContainer}
  191. {scrollContainer}
  192. </Column>
  193. );
  194. }
  195. }