Skip to content

Commit

Permalink
update hashtags timeline to only imastodon
Browse files Browse the repository at this point in the history
  • Loading branch information
lnanase committed Jan 31, 2019
1 parent 133d10a commit b84262f
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 11 deletions.
2 changes: 1 addition & 1 deletion app/javascript/mastodon/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
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 = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectHashtagStream = (tag, isLocal, accept) => connectTimelineStream(`hashtag:${tag}`, `hashtag${ isLocal ? ':local' : '' }&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
9 changes: 1 addition & 8 deletions app/javascript/mastodon/actions/timelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies }
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 });
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 const expandHashtagTimeline = (hashtag, { maxId, isLocal } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}${isLocal ? '?local=true' : ''}`, { max_id: maxId }, done);

export function expandTimelineRequest(timeline, isLoadingMore) {
return {
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/containers/timeline_container.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline';
import CommunityTimeline from '../features/standalone/community_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline';
import HashtagTimeline from '../features/standalone/imas_hashtag_timeline';
import ModalContainer from '../features/ui/containers/modal_container';
import initialState from '../initial_state';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
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>
</div>
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
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));
},

});

export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
126 changes: 126 additions & 0 deletions app/javascript/mastodon/features/imas_hashtag_timeline/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from './containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from './containers/column_settings_container';
import { expandHashtagTimeline } from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from '../../actions/streaming';
import { isEqual } from 'lodash';

const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
isLocal: state.getIn(['settings', 'tag', `${props.params.id}`, 'shows', 'local'], false),
});

export default @connect(mapStateToProps)
class HashtagTimeline extends React.PureComponent {

disconnects = [];

static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
isLocal: PropTypes.bool,
};

handlePin = () => {
const { columnId, dispatch } = this.props;

if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
}
}

handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}

handleHeaderClick = () => {
this.column.scrollTop();
}

_subscribe (dispatch, id, isLocal) {
this.disconnect = dispatch(connectHashtagStream(id, isLocal));
}

_unsubscribe () {
this.disconnects.map(disconnect => disconnect());
this.disconnects = [];
}

componentDidMount () {
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);
}
}

componentWillUnmount () {
this._unsubscribe();
}

setRef = c => {
this.column = c;
}

handleLoadMore = maxId => {
this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId, isLocal: this.props.isLocal }));
}

render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;

return (
<Column ref={this.setRef} label={`#${id}`}>
<ColumnHeader
icon='hashtag'
active={hasUnread}
title={id}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
showBackButton
>
<ColumnSettingsContainer
tag={id}
/>
</ColumnHeader>

<StatusListContainer
tag={id}
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
shouldUpdateScroll={shouldUpdateScroll}
/>
</Column>
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandHashtagTimeline } from '../../../actions/timelines';
import { connectHashtagStream } from '../../../actions/streaming';
import Masonry from 'react-masonry-infinite';
import { List as ImmutableList } from 'immutable';
import DetailedStatusContainer from '../../status/containers/detailed_status_container';
import { debounce } from 'lodash';
import LoadingIndicator from '../../../components/loading_indicator';

const mapStateToProps = (state, { hashtag }) => ({
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false),
hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false),
});

export default @connect(mapStateToProps)
class HashtagTimeline extends React.PureComponent {

static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
hashtag: PropTypes.string.isRequired,
};

componentDidMount () {
const { dispatch, hashtag } = this.props;

dispatch(expandHashtagTimeline(hashtag));
this.disconnect = dispatch(connectHashtagStream(hashtag, hashtag));
}

componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

handleLoadMore = () => {
const maxId = this.props.statusIds.last();

if (maxId) {
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
}
}

setRef = c => {
this.masonry = c;
}

handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}

this.masonry.forcePack();
}, 50)

render () {
const { statusIds, hasMore, isLoading } = this.props;

const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];

const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;

return (
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
{statusIds.map(statusId => (
<div className='statuses-grid__item' key={statusId}>
<DetailedStatusContainer
id={statusId}
compact
measureHeight
onHeightChange={this.handleHeightChange}
/>
</div>
)).toArray()}
</Masonry>
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function CommunityTimeline () {
}

export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../imas_hashtag_timeline');
}

export function DirectTimeline() {
Expand Down

0 comments on commit b84262f

Please sign in to comment.