From 48b9619439818ecb344ae33c9c31a55ecb1aa27a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 5 Nov 2016 15:20:05 +0100 Subject: Adding hashtags --- .../javascripts/components/actions/timelines.jsx | 24 ++++++-- .../components/components/status_content.jsx | 14 ++++- .../javascripts/components/containers/mastodon.jsx | 2 + .../components/features/account/index.jsx | 2 +- .../components/features/hashtag_timeline/index.jsx | 72 ++++++++++++++++++++++ .../ui/containers/status_list_container.jsx | 11 ++-- .../javascripts/components/reducers/timelines.jsx | 9 +-- 7 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/components/features/hashtag_timeline/index.jsx (limited to 'app/assets') diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 01eee1712..1dd770848 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -1,4 +1,5 @@ import api from '../api' +import Immutable from 'immutable'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -54,20 +55,25 @@ export function refreshTimelineRequest(timeline) { }; }; -export function refreshTimeline(timeline, replace = false) { +export function refreshTimeline(timeline, replace = false, id = null) { return function (dispatch, getState) { dispatch(refreshTimelineRequest(timeline)); - const ids = getState().getIn(['timelines', timeline]); + const ids = getState().getIn(['timelines', timeline], Immutable.List()); const newestId = ids.size > 0 ? ids.first() : null; let params = ''; + let path = timeline; if (newestId !== null && !replace) { params = `?since_id=${newestId}`; } - api(getState).get(`/api/v1/statuses/${timeline}${params}`).then(function (response) { + if (id) { + path = `${path}/${id}` + } + + api(getState).get(`/api/v1/statuses/${path}${params}`).then(function (response) { dispatch(refreshTimelineSuccess(timeline, response.data, replace)); }).catch(function (error) { dispatch(refreshTimelineFail(timeline, error)); @@ -83,13 +89,19 @@ export function refreshTimelineFail(timeline, error) { }; }; -export function expandTimeline(timeline) { +export function expandTimeline(timeline, id = null) { return (dispatch, getState) => { - const lastId = getState().getIn(['timelines', timeline]).last(); + const lastId = getState().getIn(['timelines', timeline], Immutable.List()).last(); dispatch(expandTimelineRequest(timeline)); - api(getState).get(`/api/v1/statuses/${timeline}?max_id=${lastId}`).then(response => { + let path = timeline; + + if (id) { + path = `${path}/${id}` + } + + api(getState).get(`/api/v1/statuses/${path}?max_id=${lastId}`).then(response => { dispatch(expandTimelineSuccess(timeline, response.data)); }).catch(error => { dispatch(expandTimelineFail(timeline, error)); diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 357465248..2006e965a 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -23,11 +23,14 @@ const StatusContent = React.createClass({ if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); + } else if (link.text[0] === '#' || (link.previousSibling && link.previousSibling.text === '#')) { + link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); } else { link.setAttribute('target', '_blank'); link.setAttribute('rel', 'noopener'); - link.addEventListener('click', this.onNormalClick, false); } + + link.addEventListener('click', this.onNormalClick, false); } }, @@ -36,8 +39,15 @@ const StatusContent = React.createClass({ e.preventDefault(); this.context.router.push(`/accounts/${mention.get('id')}`); } + }, - e.stopPropagation(); + onHashtagClick (hashtag, e) { + hashtag = hashtag.replace(/^#/, '').toLowerCase(); + + if (e.button === 0) { + e.preventDefault(); + this.context.router.push(`/statuses/tag/${hashtag}`); + } }, onNormalClick (e) { diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index bf92e248d..f29893ec0 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -30,6 +30,7 @@ import Followers from '../features/followers'; import Following from '../features/following'; import Reblogs from '../features/reblogs'; import Favourites from '../features/favourites'; +import HashtagTimeline from '../features/hashtag_timeline'; const store = configureStore(); @@ -85,6 +86,7 @@ const Mastodon = React.createClass({ + diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index 6cadcff4d..818979f8f 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -47,7 +47,7 @@ const Account = React.createClass({ this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); }, - componentWillReceiveProps(nextProps) { + componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); } diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx new file mode 100644 index 000000000..de6a9618e --- /dev/null +++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx @@ -0,0 +1,72 @@ +import { connect } from 'react-redux'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +import StatusListContainer from '../ui/containers/status_list_container'; +import Column from '../ui/components/column'; +import { + refreshTimeline, + updateTimeline +} from '../../actions/timelines'; + +const HashtagTimeline = React.createClass({ + + propTypes: { + params: React.PropTypes.object.isRequired, + dispatch: React.PropTypes.func.isRequired + }, + + mixins: [PureRenderMixin], + + _subscribe (dispatch, id) { + if (typeof App !== 'undefined') { + this.subscription = App.cable.subscriptions.create({ + channel: 'HashtagChannel', + tag: id + }, { + + received (data) { + dispatch(updateTimeline('tag', JSON.parse(data.message))); + } + + }); + } + }, + + _unsubscribe () { + if (typeof this.subscription !== 'undefined') { + this.subscription.unsubscribe(); + } + }, + + componentWillMount () { + const { dispatch } = this.props; + const { id } = this.props.params; + + dispatch(refreshTimeline('tag', true, id)); + this._subscribe(dispatch, id); + }, + + componentWillReceiveProps (nextProps) { + if (nextProps.params.id !== this.props.params.id) { + this.props.dispatch(refreshTimeline('tag', true, nextProps.params.id)); + this._unsubscribe(); + this._subscribe(this.props.dispatch, nextProps.params.id); + } + }, + + componentWillUnmount () { + this._unsubscribe(); + }, + + render () { + const { id } = this.props.params; + + return ( + + + + ); + }, + +}); + +export default connect()(HashtagTimeline); diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx index 213435a06..8004e3f04 100644 --- a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx @@ -1,15 +1,16 @@ -import { connect } from 'react-redux'; -import StatusList from '../../../components/status_list'; -import { expandTimeline } from '../../../actions/timelines'; +import { connect } from 'react-redux'; +import StatusList from '../../../components/status_list'; +import { expandTimeline } from '../../../actions/timelines'; +import Immutable from 'immutable'; const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', props.type]) + statusIds: state.getIn(['timelines', props.type], Immutable.List()) }); const mapDispatchToProps = function (dispatch, props) { return { onScrollToBottom () { - dispatch(expandTimeline(props.type)); + dispatch(expandTimeline(props.type, props.id)); } }; }; diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index c12d1b70d..9e79a4100 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -25,6 +25,7 @@ const initialState = Immutable.Map({ home: Immutable.List(), mentions: Immutable.List(), public: Immutable.List(), + tag: Immutable.List(), accounts_timelines: Immutable.Map(), ancestors: Immutable.Map(), descendants: Immutable.Map() @@ -55,7 +56,7 @@ const normalizeTimeline = (state, timeline, statuses, replace = false) => { ids = ids.set(i, status.get('id')); }); - return state.update(timeline, list => (replace ? ids : list.unshift(...ids))); + return state.update(timeline, Immutable.List(), list => (replace ? ids : list.unshift(...ids))); }; const appendNormalizedTimeline = (state, timeline, statuses) => { @@ -66,7 +67,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => { moreIds = moreIds.set(i, status.get('id')); }); - return state.update(timeline, list => list.push(...moreIds)); + return state.update(timeline, Immutable.List(), list => list.push(...moreIds)); }; const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => { @@ -94,7 +95,7 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => { const updateTimeline = (state, timeline, status, references) => { state = normalizeStatus(state, status); - state = state.update(timeline, list => { + state = state.update(timeline, Immutable.List(), list => { if (list.includes(status.get('id'))) { return list; } @@ -113,7 +114,7 @@ const updateTimeline = (state, timeline, status, references) => { const deleteStatus = (state, id, accountId, references) => { // Remove references from timelines - ['home', 'mentions', 'public'].forEach(function (timeline) { + ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) { state = state.update(timeline, list => list.filterNot(item => item === id)); }); -- cgit