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.

217 lines
7.1 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 StatusListContainer from '../ui/containers/status_list_container';
  6. import Column from '../../components/column';
  7. import ColumnBackButton from '../../components/column_back_button';
  8. import ColumnHeader from '../../components/column_header';
  9. import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
  10. import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
  11. import { connectListStream } from '../../actions/streaming';
  12. import { expandListTimeline } from '../../actions/timelines';
  13. import { fetchList, deleteList, updateList } from '../../actions/lists';
  14. import { openModal } from '../../actions/modal';
  15. import MissingIndicator from '../../components/missing_indicator';
  16. import LoadingIndicator from '../../components/loading_indicator';
  17. import Icon from 'mastodon/components/icon';
  18. import RadioButton from 'mastodon/components/radio_button';
  19. const messages = defineMessages({
  20. deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
  21. deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
  22. followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
  23. none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
  24. list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
  25. });
  26. const mapStateToProps = (state, props) => ({
  27. list: state.getIn(['lists', props.params.id]),
  28. hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
  29. });
  30. export default @connect(mapStateToProps)
  31. @injectIntl
  32. class ListTimeline extends React.PureComponent {
  33. static contextTypes = {
  34. router: PropTypes.object,
  35. };
  36. static propTypes = {
  37. params: PropTypes.object.isRequired,
  38. dispatch: PropTypes.func.isRequired,
  39. shouldUpdateScroll: PropTypes.func,
  40. columnId: PropTypes.string,
  41. hasUnread: PropTypes.bool,
  42. multiColumn: PropTypes.bool,
  43. list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
  44. intl: PropTypes.object.isRequired,
  45. };
  46. handlePin = () => {
  47. const { columnId, dispatch } = this.props;
  48. if (columnId) {
  49. dispatch(removeColumn(columnId));
  50. } else {
  51. dispatch(addColumn('LIST', { id: this.props.params.id }));
  52. this.context.router.history.push('/');
  53. }
  54. }
  55. handleMove = (dir) => {
  56. const { columnId, dispatch } = this.props;
  57. dispatch(moveColumn(columnId, dir));
  58. }
  59. handleHeaderClick = () => {
  60. this.column.scrollTop();
  61. }
  62. componentDidMount () {
  63. const { dispatch } = this.props;
  64. const { id } = this.props.params;
  65. dispatch(fetchList(id));
  66. dispatch(expandListTimeline(id));
  67. this.disconnect = dispatch(connectListStream(id));
  68. }
  69. componentWillReceiveProps (nextProps) {
  70. const { dispatch } = this.props;
  71. const { id } = nextProps.params;
  72. if (id !== this.props.params.id) {
  73. if (this.disconnect) {
  74. this.disconnect();
  75. this.disconnect = null;
  76. }
  77. dispatch(fetchList(id));
  78. dispatch(expandListTimeline(id));
  79. this.disconnect = dispatch(connectListStream(id));
  80. }
  81. }
  82. componentWillUnmount () {
  83. if (this.disconnect) {
  84. this.disconnect();
  85. this.disconnect = null;
  86. }
  87. }
  88. setRef = c => {
  89. this.column = c;
  90. }
  91. handleLoadMore = maxId => {
  92. const { id } = this.props.params;
  93. this.props.dispatch(expandListTimeline(id, { maxId }));
  94. }
  95. handleEditClick = () => {
  96. this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
  97. }
  98. handleDeleteClick = () => {
  99. const { dispatch, columnId, intl } = this.props;
  100. const { id } = this.props.params;
  101. dispatch(openModal('CONFIRM', {
  102. message: intl.formatMessage(messages.deleteMessage),
  103. confirm: intl.formatMessage(messages.deleteConfirm),
  104. onConfirm: () => {
  105. dispatch(deleteList(id));
  106. if (!!columnId) {
  107. dispatch(removeColumn(columnId));
  108. } else {
  109. this.context.router.history.push('/lists');
  110. }
  111. },
  112. }));
  113. }
  114. handleRepliesPolicyChange = ({ target }) => {
  115. const { dispatch } = this.props;
  116. const { id } = this.props.params;
  117. dispatch(updateList(id, undefined, false, target.value));
  118. }
  119. render () {
  120. const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
  121. const { id } = this.props.params;
  122. const pinned = !!columnId;
  123. const title = list ? list.get('title') : id;
  124. const replies_policy = list ? list.get('replies_policy') : undefined;
  125. if (typeof list === 'undefined') {
  126. return (
  127. <Column>
  128. <div className='scrollable'>
  129. <LoadingIndicator />
  130. </div>
  131. </Column>
  132. );
  133. } else if (list === false) {
  134. return (
  135. <Column>
  136. <ColumnBackButton multiColumn={multiColumn} />
  137. <MissingIndicator />
  138. </Column>
  139. );
  140. }
  141. return (
  142. <Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
  143. <ColumnHeader
  144. icon='list-ul'
  145. active={hasUnread}
  146. title={title}
  147. onPin={this.handlePin}
  148. onMove={this.handleMove}
  149. onClick={this.handleHeaderClick}
  150. pinned={pinned}
  151. multiColumn={multiColumn}
  152. >
  153. <div className='column-settings__row column-header__links'>
  154. <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
  155. <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
  156. </button>
  157. <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
  158. <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
  159. </button>
  160. </div>
  161. { replies_policy !== undefined && (
  162. <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
  163. <span id={`list-${id}-replies-policy`} className='column-settings__section'>
  164. <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
  165. </span>
  166. <div className='column-settings__row'>
  167. { ['none', 'list', 'followed'].map(policy => (
  168. <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
  169. ))}
  170. </div>
  171. </div>
  172. )}
  173. </ColumnHeader>
  174. <StatusListContainer
  175. trackScroll={!pinned}
  176. scrollKey={`list_timeline-${columnId}`}
  177. timelineId={`list:${id}`}
  178. onLoadMore={this.handleLoadMore}
  179. emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
  180. shouldUpdateScroll={shouldUpdateScroll}
  181. bindToDocument={!multiColumn}
  182. />
  183. </Column>
  184. );
  185. }
  186. }