From 4be73132982fec0a38420811ae28a4ffd2ea09c7 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 18 Dec 2018 17:56:08 +0100 Subject: [Glitch] Allow joining several hashtags in a single column Port 4c03e05a4e1a237f8a414a0861c03abe3269dbc8 to glitch-soc This introduces new requirements in the API: `/api/v1/timelines/tag/:tag` now accepts new params: `any`, `all` and `none` It now returns status matching tag :tag or any of the :any, provided that they also include all tags in `all` and none of `none`. --- .../hashtag_timeline/components/column_settings.js | 102 +++++++++++++++++++++ .../containers/column_settings_container.js | 31 +++++++ .../glitch/features/hashtag_timeline/index.js | 72 +++++++++++---- .../features/standalone/hashtag_timeline/index.js | 2 +- 4 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js create mode 100644 app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js (limited to 'app/javascript/flavours/glitch/features') diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js new file mode 100644 index 000000000..82936c838 --- /dev/null +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js @@ -0,0 +1,102 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import Toggle from 'react-toggle'; +import AsyncSelect from 'react-select/lib/Async'; + +@injectIntl +export default class ColumnSettings extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + onLoad: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + open: this.hasTags(), + }; + + hasTags () { + return ['all', 'any', 'none'].map(mode => this.tags(mode).length > 0).includes(true); + } + + tags (mode) { + let tags = this.props.settings.getIn(['tags', mode]) || []; + if (tags.toJSON) { + return tags.toJSON(); + } else { + return tags; + } + }; + + onSelect = (mode) => { + return (value) => { + this.props.onChange(['tags', mode], value); + }; + }; + + onToggle = () => { + if (this.state.open && this.hasTags()) { + this.props.onChange('tags', {}); + } + this.setState({ open: !this.state.open }); + }; + + modeSelect (mode) { + return ( +
+ {this.modeLabel(mode)} + +
+ ); + } + + modeLabel (mode) { + switch(mode) { + case 'any': return ; + case 'all': return ; + case 'none': return ; + } + return ''; + }; + + render () { + return ( +
+
+
+ + + + +
+
+ {this.state.open && +
+ {this.modeSelect('any')} + {this.modeSelect('all')} + {this.modeSelect('none')} +
+ } +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js new file mode 100644 index 000000000..757cd48fb --- /dev/null +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../components/column_settings'; +import { changeColumnParams } from 'flavours/glitch/actions/columns'; +import api from 'flavours/glitch/util/api'; + +const mapStateToProps = (state, { columnId }) => { + const columns = state.getIn(['settings', 'columns']); + const index = columns.findIndex(c => c.get('uuid') === columnId); + + if (!(columnId && index >= 0)) { + return {}; + } + + return { settings: columns.get(index).get('params') }; +}; + +const mapDispatchToProps = (dispatch, { columnId }) => ({ + onChange (key, value) { + dispatch(changeColumnParams(columnId, key, value)); + }, + + onLoad (value) { + return api().get('/api/v2/search', { params: { q: value } }).then(response => { + return (response.data.hashtags || []).map((tag) => { + return { value: tag.name, label: `#${tag.name}` }; + }); + }); + }, +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 311fabb63..4455f8957 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -4,7 +4,8 @@ import PropTypes from 'prop-types'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandHashtagTimeline } from 'flavours/glitch/actions/timelines'; +import ColumnSettingsContainer from './containers/column_settings_container'; +import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { FormattedMessage } from 'react-intl'; import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; @@ -16,6 +17,8 @@ const mapStateToProps = (state, props) => ({ @connect(mapStateToProps) export default class HashtagTimeline extends React.PureComponent { + disconnects = []; + static propTypes = { params: PropTypes.object.isRequired, columnId: PropTypes.string, @@ -34,6 +37,30 @@ export default class HashtagTimeline extends React.PureComponent { } } + title = () => { + let title = [this.props.params.id]; + if (this.additionalFor('any')) { + title.push(); + } + if (this.additionalFor('all')) { + title.push(); + } + if (this.additionalFor('none')) { + title.push(); + } + return title; + } + + additionalFor = (mode) => { + const { tags } = this.props.params; + + if (tags && (tags[mode] || []).length > 0) { + return tags[mode].map(tag => tag.value).join('/'); + } else { + return ''; + } + } + handleMove = (dir) => { const { columnId, dispatch } = this.props; dispatch(moveColumn(columnId, dir)); @@ -43,30 +70,40 @@ export default class HashtagTimeline extends React.PureComponent { this.column.scrollTop(); } - _subscribe (dispatch, id) { - this.disconnect = dispatch(connectHashtagStream(id)); + _subscribe (dispatch, id, tags = {}) { + let any = (tags.any || []).map(tag => tag.value); + let all = (tags.all || []).map(tag => tag.value); + let none = (tags.none || []).map(tag => tag.value); + + [id, ...any].map((tag) => { + this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => { + let tags = status.tags.map(tag => tag.name); + return all.filter(tag => tags.includes(tag)).length === all.length && + none.filter(tag => tags.includes(tag)).length === 0; + }))); + }); } _unsubscribe () { - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } + this.disconnects.map(disconnect => disconnect()); + this.disconnects = []; } componentDidMount () { const { dispatch } = this.props; - const { id } = this.props.params; + const { id, tags } = this.props.params; - dispatch(expandHashtagTimeline(id)); - this._subscribe(dispatch, id); + dispatch(expandHashtagTimeline(id, { tags })); } componentWillReceiveProps (nextProps) { - if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(expandHashtagTimeline(nextProps.params.id)); + const { dispatch, params } = this.props; + const { id, tags } = nextProps.params; + if (id !== params.id || tags !== params.tags) { this._unsubscribe(); - this._subscribe(this.props.dispatch, nextProps.params.id); + this._subscribe(dispatch, id, tags); + this.props.dispatch(clearTimeline(`hashtag:${id}`)); + this.props.dispatch(expandHashtagTimeline(id, { tags })); } } @@ -79,7 +116,8 @@ export default class HashtagTimeline extends React.PureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId })); + const { id, tags } = this.props.params; + this.props.dispatch(expandHashtagTimeline(id, { maxId, tags })); } render () { @@ -92,14 +130,16 @@ export default class HashtagTimeline extends React.PureComponent { + > + {columnId && } +