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.

221 lines
7.3 KiB

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