about summary refs log tree commit diff
path: root/app/assets
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-11-05 15:20:05 +0100
committerEugen Rochko <eugen@zeonfederated.com>2016-11-05 17:13:14 +0100
commit48b9619439818ecb344ae33c9c31a55ecb1aa27a (patch)
treeef350e3ebf2563adbe4d57778a08f561d912415f /app/assets
parent62292797eccc5bcf47abae9f4daaa2c186660644 (diff)
Adding hashtags
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/components/actions/timelines.jsx24
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx14
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx2
-rw-r--r--app/assets/javascripts/components/features/account/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx72
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx11
-rw-r--r--app/assets/javascripts/components/reducers/timelines.jsx9
7 files changed, 116 insertions, 18 deletions
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({
             <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} />
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 (
+      <Column icon='hashtag' heading={id}>
+        <StatusListContainer type='tag' id={id} />
+      </Column>
+    );
+  },
+
+});
+
+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));
   });