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.

193 lines
6.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. } from 'flavours/glitch/actions/notifications';
  12. import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
  13. import NotificationContainer from './containers/notification_container';
  14. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  15. import ColumnSettingsContainer from './containers/column_settings_container';
  16. import { createSelector } from 'reselect';
  17. import { List as ImmutableList } from 'immutable';
  18. import { debounce } from 'lodash';
  19. import ScrollableList from 'flavours/glitch/components/scrollable_list';
  20. const messages = defineMessages({
  21. title: { id: 'column.notifications', defaultMessage: 'Notifications' },
  22. });
  23. const getNotifications = createSelector([
  24. state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
  25. state => state.getIn(['notifications', 'items']),
  26. ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
  27. const mapStateToProps = state => ({
  28. notifications: getNotifications(state),
  29. localSettings: state.get('local_settings'),
  30. isLoading: state.getIn(['notifications', 'isLoading'], true),
  31. isUnread: state.getIn(['notifications', 'unread']) > 0,
  32. hasMore: !!state.getIn(['notifications', 'next']),
  33. notifCleaningActive: state.getIn(['notifications', 'cleaningMode']),
  34. });
  35. /* glitch */
  36. const mapDispatchToProps = dispatch => ({
  37. onEnterCleaningMode(yes) {
  38. dispatch(enterNotificationClearingMode(yes));
  39. },
  40. dispatch,
  41. });
  42. @connect(mapStateToProps, mapDispatchToProps)
  43. @injectIntl
  44. export default class Notifications extends React.PureComponent {
  45. static propTypes = {
  46. columnId: PropTypes.string,
  47. notifications: ImmutablePropTypes.list.isRequired,
  48. dispatch: PropTypes.func.isRequired,
  49. shouldUpdateScroll: PropTypes.func,
  50. intl: PropTypes.object.isRequired,
  51. isLoading: PropTypes.bool,
  52. isUnread: PropTypes.bool,
  53. multiColumn: PropTypes.bool,
  54. hasMore: PropTypes.bool,
  55. localSettings: ImmutablePropTypes.map,
  56. notifCleaningActive: PropTypes.bool,
  57. onEnterCleaningMode: PropTypes.func,
  58. };
  59. static defaultProps = {
  60. trackScroll: true,
  61. };
  62. handleScrollToBottom = debounce(() => {
  63. this.props.dispatch(scrollTopNotifications(false));
  64. this.props.dispatch(expandNotifications());
  65. }, 300, { leading: true });
  66. handleScrollToTop = debounce(() => {
  67. this.props.dispatch(scrollTopNotifications(true));
  68. }, 100);
  69. handleScroll = debounce(() => {
  70. this.props.dispatch(scrollTopNotifications(false));
  71. }, 100);
  72. handlePin = () => {
  73. const { columnId, dispatch } = this.props;
  74. if (columnId) {
  75. dispatch(removeColumn(columnId));
  76. } else {
  77. dispatch(addColumn('NOTIFICATIONS', {}));
  78. }
  79. }
  80. handleMove = (dir) => {
  81. const { columnId, dispatch } = this.props;
  82. dispatch(moveColumn(columnId, dir));
  83. }
  84. handleHeaderClick = () => {
  85. this.column.scrollTop();
  86. }
  87. setColumnRef = c => {
  88. this.column = c;
  89. }
  90. handleMoveUp = id => {
  91. const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) - 1;
  92. this._selectChild(elementIndex);
  93. }
  94. handleMoveDown = id => {
  95. const elementIndex = this.props.notifications.findIndex(item => item.get('id') === id) + 1;
  96. this._selectChild(elementIndex);
  97. }
  98. _selectChild (index) {
  99. const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
  100. if (element) {
  101. element.focus();
  102. }
  103. }
  104. render () {
  105. const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
  106. const pinned = !!columnId;
  107. const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
  108. let scrollableContent = null;
  109. if (isLoading && this.scrollableContent) {
  110. scrollableContent = this.scrollableContent;
  111. } else if (notifications.size > 0 || hasMore) {
  112. scrollableContent = notifications.map((item) => (
  113. <NotificationContainer
  114. key={item.get('id')}
  115. notification={item}
  116. accountId={item.get('account')}
  117. onMoveUp={this.handleMoveUp}
  118. onMoveDown={this.handleMoveDown}
  119. />
  120. ));
  121. } else {
  122. scrollableContent = null;
  123. }
  124. this.scrollableContent = scrollableContent;
  125. const scrollContainer = (
  126. <ScrollableList
  127. scrollKey={`notifications-${columnId}`}
  128. trackScroll={!pinned}
  129. isLoading={isLoading}
  130. hasMore={hasMore}
  131. emptyMessage={emptyMessage}
  132. onScrollToBottom={this.handleScrollToBottom}
  133. onScrollToTop={this.handleScrollToTop}
  134. onScroll={this.handleScroll}
  135. shouldUpdateScroll={shouldUpdateScroll}
  136. >
  137. {scrollableContent}
  138. </ScrollableList>
  139. );
  140. return (
  141. <Column
  142. ref={this.setColumnRef}
  143. name='notifications'
  144. extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
  145. >
  146. <ColumnHeader
  147. icon='bell'
  148. active={isUnread}
  149. title={intl.formatMessage(messages.title)}
  150. onPin={this.handlePin}
  151. onMove={this.handleMove}
  152. onClick={this.handleHeaderClick}
  153. pinned={pinned}
  154. multiColumn={multiColumn}
  155. localSettings={this.props.localSettings}
  156. notifCleaning
  157. notifCleaningActive={this.props.notifCleaningActive} // this is used to toggle the header text
  158. onEnterCleaningMode={this.props.onEnterCleaningMode}
  159. >
  160. <ColumnSettingsContainer />
  161. </ColumnHeader>
  162. {scrollContainer}
  163. </Column>
  164. );
  165. }
  166. }