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.

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