Adding hashtags

This commit is contained in:
Eugen Rochko
2016-11-05 15:20:05 +01:00
parent 62292797ec
commit 48b9619439
33 changed files with 305 additions and 62 deletions

View File

@ -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));

View File

@ -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) {

View File

@ -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({
<Route path='/statuses/home' component={HomeTimeline} />
<Route path='/statuses/mentions' component={MentionsTimeline} />
<Route path='/statuses/all' component={PublicTimeline} />
<Route path='/statuses/tag/:id' component={HashtagTimeline} />
<Route path='/statuses/:statusId' component={Status} />
<Route path='/statuses/:statusId/reblogs' component={Reblogs} />

View File

@ -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)));
}

View File

@ -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 (
<Column icon='hashtag' heading={id}>
<StatusListContainer type='tag' id={id} />
</Column>
);
},
});
export default connect()(HashtagTimeline);

View File

@ -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));
}
};
};

View File

@ -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));
});