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.

254 lines
8.3 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, true);
  120. }
  121. handleMoveDown = id => {
  122. const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
  123. this._selectChild(elementIndex, false);
  124. }
  125. _selectChild (index, align_top) {
  126. const container = this.column.node;
  127. const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
  128. if (element) {
  129. if (align_top && container.scrollTop > element.offsetTop) {
  130. element.scrollIntoView(true);
  131. } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
  132. element.scrollIntoView(false);
  133. }
  134. element.focus();
  135. }
  136. }
  137. componentDidMount () {
  138. const { onMount } = this.props;
  139. if (onMount) {
  140. onMount();
  141. }
  142. }
  143. componentWillUnmount () {
  144. const { onUnmount } = this.props;
  145. if (onUnmount) {
  146. onUnmount();
  147. }
  148. }
  149. render () {
  150. const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props;
  151. const pinned = !!columnId;
  152. const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
  153. let scrollableContent = null;
  154. const filterBarContainer = showFilterBar
  155. ? (<FilterBarContainer />)
  156. : null;
  157. if (isLoading && this.scrollableContent) {
  158. scrollableContent = this.scrollableContent;
  159. } else if (notifications.size > 0 || hasMore) {
  160. scrollableContent = notifications.map((item, index) => item === null ? (
  161. <LoadGap
  162. key={'gap:' + notifications.getIn([index + 1, 'id'])}
  163. disabled={isLoading}
  164. maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
  165. onClick={this.handleLoadGap}
  166. />
  167. ) : (
  168. <NotificationContainer
  169. key={item.get('id')}
  170. notification={item}
  171. accountId={item.get('account')}
  172. onMoveUp={this.handleMoveUp}
  173. onMoveDown={this.handleMoveDown}
  174. />
  175. ));
  176. } else {
  177. scrollableContent = null;
  178. }
  179. this.scrollableContent = scrollableContent;
  180. const scrollContainer = (
  181. <ScrollableList
  182. scrollKey={`notifications-${columnId}`}
  183. trackScroll={!pinned}
  184. isLoading={isLoading}
  185. showLoading={isLoading && notifications.size === 0}
  186. hasMore={hasMore}
  187. emptyMessage={emptyMessage}
  188. onLoadMore={this.handleLoadOlder}
  189. onScrollToTop={this.handleScrollToTop}
  190. onScroll={this.handleScroll}
  191. shouldUpdateScroll={shouldUpdateScroll}
  192. >
  193. {scrollableContent}
  194. </ScrollableList>
  195. );
  196. return (
  197. <Column
  198. ref={this.setColumnRef}
  199. name='notifications'
  200. extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
  201. label={intl.formatMessage(messages.title)}
  202. >
  203. <ColumnHeader
  204. icon='bell'
  205. active={isUnread}
  206. title={intl.formatMessage(messages.title)}
  207. onPin={this.handlePin}
  208. onMove={this.handleMove}
  209. onClick={this.handleHeaderClick}
  210. pinned={pinned}
  211. multiColumn={multiColumn}
  212. localSettings={this.props.localSettings}
  213. notifCleaning
  214. notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text
  215. onEnterCleaningMode={this.props.onEnterCleaningMode}
  216. >
  217. <ColumnSettingsContainer />
  218. </ColumnHeader>
  219. {filterBarContainer}
  220. {scrollContainer}
  221. </Column>
  222. );
  223. }
  224. }