diff --cc app/javascript/mastodon/actions/streaming.js
index 68279c4b3,cd319709d..000000000
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@@ -51,6 -51,6 +51,6 @@@ const refreshHomeTimelineAndNotificatio
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
- export const connectHashtagStream = (tag, isLocal) => connectTimelineStream(`hashtag:${tag}`, `hashtag${ isLocal ? ':local' : '' }&tag=${tag}`);
-export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
++export const connectHashtagStream = (id, tag, isLocal, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag${ isLocal ? ':local' : '' }&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --cc app/javascript/mastodon/actions/timelines.js
index a693f4ca7,6e7bd027c..000000000
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@@ -79,10 -96,17 +96,21 @@@ export const expandCommunityTimelin
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
++<<<<<<< HEAD
+export const expandHashtagTimeline = (hashtag, { maxId, isLocal } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}${isLocal ? '?local=true' : ''}`, { max_id: maxId }, done);
++=======
++>>>>>>> v2.7.0
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+ export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
+ return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
+ max_id: maxId,
+ any: parseTags(tags, 'any'),
+ all: parseTags(tags, 'all'),
+ none: parseTags(tags, 'none'),
+ }, done);
+ };
- export function expandTimelineRequest(timeline) {
+ export function expandTimelineRequest(timeline, isLoadingMore) {
return {
type: TIMELINE_EXPAND_REQUEST,
timeline,
diff --cc app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
index f844a52f4,9c9f62d82..000000000
--- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
@@@ -1,93 -1,100 +1,190 @@@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
++<<<<<<< HEAD
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Button from '../../../components/button';
+import SettingToggle from '../components/setting_toggle';
+import SettingText from '../components/setting_text';
+import { Map as ImmutableMap } from 'immutable';
+
+const messages = defineMessages({
+ filter_regex: { id: 'tag.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
+ show_local_only: { id: 'tag.column_settings.show_local_only', defaultMessage: 'Show local only' },
+ settings: { id: 'tag.settings', defaultMessage: 'Column settings' },
+ add_favourite_tags_public: { id: 'tag.add_favourite.public', defaultMessage: 'add in the favourite tags (Public)' },
+ add_favourite_tags_unlisted: { id: 'tag.add_favourite.unlisted', defaultMessage: 'add in the favourite tags (Unlisted)' },
+ remove_favourite_tags: { id: 'tag.remove_favourite', defaultMessage: 'Remove from the favourite tags' },
+});
+
+@injectIntl
+export default class ColumnSettings extends React.PureComponent {
+
+ static propTypes = {
+ tag: PropTypes.string.isRequired,
+ settings: ImmutablePropTypes.map.isRequired,
+ onChange: PropTypes.func.isRequired,
+ addFavouriteTags: PropTypes.func.isRequired,
+ removeFavouriteTags: PropTypes.func.isRequired,
+ isRegistered: PropTypes.bool.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ addFavouriteTags = (visibility) => {
+ this.props.addFavouriteTags(this.props.tag, visibility);
+ };
+
+ addPublic = () => {
+ this.addFavouriteTags('public');
+ };
+
+ addUnlisted = () => {
+ this.addFavouriteTags('unlisted');
+ };
+
+ removeFavouriteTags = () => {
+ this.props.removeFavouriteTags(this.props.tag);
+ };
+
+ render () {
+ const { tag, settings, onChange, intl, isRegistered } = this.props;
+ const initialSettings = ImmutableMap({
+ shows: ImmutableMap({
+ local: false,
+ }),
+
+ regex: ImmutableMap({
+ body: '',
+ }),
+ });
+
+ const favouriteTagButton = (isRegistered) => {
+ if(isRegistered) {
+ return (
+ <div className='column-settings__row'>
+ <Button className='favourite-tags__remove-button-in-column' text={intl.formatMessage(messages.remove_favourite_tags)} onClick={this.removeFavouriteTags} block />
+ </div>
+ );
+ } else {
+ return (
+ <div className='column-settings__row'>
+ <Button className='favourite-tags__add-button-in-column' text={intl.formatMessage(messages.add_favourite_tags_public)} onClick={this.addPublic} block />
+ <Button className='favourite-tags__add-button-in-column' text={intl.formatMessage(messages.add_favourite_tags_unlisted)} onClick={this.addUnlisted} block />
+ </div>
+ );
+ }
+ };
+
+ return (
+ <div>
+ {favouriteTagButton(isRegistered)}
+ <span className='column-settings__section'><FormattedMessage id='tag.column_settings.basic' defaultMessage='Basic' /></span>
+
+ <div className='column-settings__row'>
+ <SettingToggle tag={tag} prefix='hashtag_timeline' settings={settings.get(`${tag}`, initialSettings)} settingKey={['shows', 'local']} onChange={onChange} label={intl.formatMessage(messages.show_local_only)} />
+ </div>
+
+ <span className='column-settings__section'><FormattedMessage id='tag.column_settings.advanced' defaultMessage='Advanced' /></span>
+
+ <div className='column-settings__row'>
+ <SettingText tag={tag} prefix='hashtag_timeline' settings={settings.get(`${tag}`, initialSettings)} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
+ </div>
++=======
+ import { injectIntl, FormattedMessage } from 'react-intl';
+ import Toggle from 'react-toggle';
+ import AsyncSelect from 'react-select/lib/Async';
+
+ export default @injectIntl
+ 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 (
+ <div className='column-settings__section'>
+ {this.modeLabel(mode)}
+ <AsyncSelect
+ isMulti
+ autoFocus
+ value={this.tags(mode)}
+ settings={this.props.settings}
+ settingPath={['tags', mode]}
+ onChange={this.onSelect(mode)}
+ loadOptions={this.props.onLoad}
+ classNamePrefix='column-settings__hashtag-select'
+ name='tags'
+ />
+ </div>
+ );
+ }
+
+ modeLabel (mode) {
+ switch(mode) {
+ case 'any': return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
+ case 'all': return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
+ case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
+ }
+ return '';
+ };
+
+ render () {
+ return (
+ <div>
+ <div className='column-settings__row'>
+ <div className='setting-toggle'>
+ <Toggle
+ id='hashtag.column_settings.tag_toggle'
+ onChange={this.onToggle}
+ checked={this.state.open}
+ />
+ <span className='setting-toggle__label'>
+ <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
+ </span>
+ </div>
+ </div>
+ {this.state.open &&
+ <div className='column-settings__hashtags'>
+ {this.modeSelect('any')}
+ {this.modeSelect('all')}
+ {this.modeSelect('none')}
+ </div>
+ }
++>>>>>>> v2.7.0
</div>
);
}
diff --cc app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
index f1aca44dd,c5098052c..000000000
--- a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
@@@ -1,31 -1,31 +1,60 @@@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
++<<<<<<< HEAD
+import { changeSetting, saveSettings } from '../../../actions/settings';
+import { addFavouriteTags, removeFavouriteTags } from '../../../actions/favourite_tags';
+
+const mapStateToProps = (state, { tag }) => ({
+ settings: state.getIn(['settings', 'tag']),
+ isRegistered: state.getIn(['favourite_tags', 'tags']).some(t => t.get('name') === tag),
+});
+
+const mapDispatchToProps = dispatch => ({
+
+ onChange (tag, key, checked) {
+ dispatch(changeSetting(['tag', `${tag}`, ...key], checked));
+ },
+
+ onSave () {
+ dispatch(saveSettings());
+ },
+
+ addFavouriteTags (tag, visibility) {
+ dispatch(addFavouriteTags(tag, visibility));
+ },
+
+ removeFavouriteTags (tag) {
+ dispatch(removeFavouriteTags(tag));
+ },
+
++=======
+ import { changeColumnParams } from '../../../actions/columns';
+ import api from '../../../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}` };
+ });
+ });
+ },
++>>>>>>> v2.7.0
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --cc app/javascript/mastodon/features/hashtag_timeline/index.js
index b877f6700,c2e026d13..000000000
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@@ -5,7 -5,7 +5,11 @@@ import StatusListContainer from './cont
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from './containers/column_settings_container';
++<<<<<<< HEAD
+import { expandHashtagTimeline } from '../../actions/timelines';
++=======
+ import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
++>>>>>>> v2.7.0
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from '../../actions/streaming';
@@@ -47,8 -72,18 +78,23 @@@ class HashtagTimeline extends React.Pur
this.column.scrollTop();
}
++<<<<<<< HEAD
+ _subscribe (dispatch, id, isLocal) {
+ this.disconnect = dispatch(connectHashtagStream(id, isLocal));
++=======
+ _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;
+ })));
+ });
++>>>>>>> v2.7.0
}
_unsubscribe () {
@@@ -59,18 -92,20 +103,35 @@@
}
componentDidMount () {
++<<<<<<< HEAD
+ const { dispatch, isLocal } = this.props;
+ const { id } = this.props.params;
+
+ dispatch(expandHashtagTimeline(id, isLocal));
+ this._subscribe(dispatch, id, isLocal);
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.params.id !== this.props.params.id || nextProps.isLocal !== this.props.isLocal) {
+ this.props.dispatch(expandHashtagTimeline(nextProps.params.id, nextProps.isLocal));
+ this._unsubscribe();
+ this._subscribe(this.props.dispatch, nextProps.params.id, nextProps.isLocal);
++=======
+ const { dispatch } = this.props;
+ const { id, tags } = this.props.params;
+
+ dispatch(expandHashtagTimeline(id, { tags }));
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { dispatch, params } = this.props;
+ const { id, tags } = nextProps.params;
+ if (id !== params.id || !isEqual(tags, params.tags)) {
+ this._unsubscribe();
+ this._subscribe(dispatch, id, tags);
+ this.props.dispatch(clearTimeline(`hashtag:${id}`));
+ this.props.dispatch(expandHashtagTimeline(id, { tags }));
++>>>>>>> v2.7.0
}
}
@@@ -83,7 -118,8 +144,12 @@@
}
handleLoadMore = maxId => {
++<<<<<<< HEAD
+ this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId, isLocal: this.props.isLocal }));
++=======
+ const { id, tags } = this.props.params;
+ this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
++>>>>>>> v2.7.0
}
render () {
@@@ -104,9 -140,7 +170,13 @@@
multiColumn={multiColumn}
showBackButton
>
++<<<<<<< HEAD
+ <ColumnSettingsContainer
+ tag={id}
+ />
++=======
+ {columnId && <ColumnSettingsContainer columnId={columnId} />}
++>>>>>>> v2.7.0
</ColumnHeader>
<StatusListContainer