about summary refs log tree commit diff
path: root/app/assets/javascripts/components/features
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/components/features')
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx2
-rw-r--r--app/assets/javascripts/components/features/blocks/index.jsx68
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx25
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx4
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx6
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx46
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx21
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx9
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx41
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/ui/components/column.jsx10
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx112
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx7
13 files changed, 288 insertions, 67 deletions
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index dead11265..30e0449c5 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -44,7 +44,7 @@ const Header = React.createClass({
             <IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
           </div>
         );
-      } else {
+      } else if (!account.getIn(['relationship', 'blocking'])) {
         actionBtn = (
           <div style={{ position: 'absolute', top: '10px', left: '20px' }}>
             <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />
diff --git a/app/assets/javascripts/components/features/blocks/index.jsx b/app/assets/javascripts/components/features/blocks/index.jsx
new file mode 100644
index 000000000..e941b27f7
--- /dev/null
+++ b/app/assets/javascripts/components/features/blocks/index.jsx
@@ -0,0 +1,68 @@
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import { ScrollContainer } from 'react-router-scroll';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import AccountContainer from '../../containers/account_container';
+import { fetchBlocks, expandBlocks } from '../../actions/blocks';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'blocks', 'items'])
+});
+
+const Blocks = React.createClass({
+  propTypes: {
+    params: React.PropTypes.object.isRequired,
+    dispatch: React.PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  componentWillMount () {
+    this.props.dispatch(fetchBlocks());
+  },
+
+  handleScroll (e) {
+    const { scrollTop, scrollHeight, clientHeight } = e.target;
+
+    if (scrollTop === scrollHeight - clientHeight) {
+      this.props.dispatch(expandBlocks());
+    }
+  },
+
+  render () {
+    const { intl, accountIds } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    return (
+      <Column icon='users' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+        <ScrollContainer scrollKey='blocks'>
+          <div className='scrollable' onScroll={this.handleScroll}>
+            {accountIds.map(id =>
+              <AccountContainer key={id} id={id} />
+            )}
+          </div>
+        </ScrollContainer>
+      </Column>
+    );
+  }
+});
+
+export default connect(mapStateToProps)(injectIntl(Blocks));
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index 5073c9d9e..48939054d 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -117,9 +117,10 @@ const ComposeForm = React.createClass({
   },
 
   render () {
-    const { intl } = this.props;
-    let replyArea  = '';
-    const disabled = this.props.is_submitting || this.props.is_uploading;
+    const { intl }  = this.props;
+    let replyArea   = '';
+    let publishText = '';
+    const disabled  = this.props.is_submitting || this.props.is_uploading;
 
     if (this.props.in_reply_to) {
       replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
@@ -127,6 +128,12 @@ const ComposeForm = React.createClass({
 
     let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
 
+    if (this.props.private) {
+      publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
+    } else {
+      publishText = intl.formatMessage(messages.publish) + (!this.props.unlisted ? '!' : '');
+    }
+
     return (
       <div style={{ padding: '10px' }}>
         <Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
@@ -154,19 +161,19 @@ const ComposeForm = React.createClass({
         />
 
         <div style={{ marginTop: '10px', overflow: 'hidden' }}>
-          <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div>
+          <div style={{ float: 'right' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
           <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
           <UploadButtonContainer style={{ paddingTop: '4px' }} />
         </div>
 
         <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #282c37', paddingTop: '10px' }}>
-          <Toggle checked={this.props.private} onChange={this.handleChangeVisibility} />
-          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
+          <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} />
+          <span style={{ display: 'inline-block', verticalAlign: 'top', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
         </label>
 
-        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle' }}>
-          <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} />
-          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
+        <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', borderTop: '1px solid #282c37', paddingTop: '10px' }}>
+          <Toggle checked={this.props.private} onChange={this.handleChangeVisibility} />
+          <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
         </label>
 
         <Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}>
diff --git a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
index 8ccfce059..c027875cd 100644
--- a/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx
@@ -26,14 +26,14 @@ const makeMapStateToProps = () => {
       sensitive: state.getIn(['compose', 'sensitive']),
       spoiler: state.getIn(['compose', 'spoiler']),
       spoiler_text: state.getIn(['compose', 'spoiler_text']),
-      unlisted: state.getIn(['compose', 'unlisted']),
+      unlisted: state.getIn(['compose', 'unlisted'], ),
       private: state.getIn(['compose', 'private']),
       fileDropDate: state.getIn(['compose', 'fileDropDate']),
       is_submitting: state.getIn(['compose', 'is_submitting']),
       is_uploading: state.getIn(['compose', 'is_uploading']),
       in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
       media_count: state.getIn(['compose', 'media_attachments']).size,
-      me: state.getIn(['compose', 'me'])
+      me: state.getIn(['compose', 'me']),
     };
   };
 
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index 42e0a9e24..a0bf3a694 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -11,7 +11,9 @@ const messages = defineMessages({
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
   sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Sign out' },
-  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
+  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+  info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }
 });
 
 const mapStateToProps = state => ({
@@ -32,6 +34,8 @@ const GettingStarted = ({ intl, me }) => {
         <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
         <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
         {followRequests}
+        <ColumnLink icon='users' text={intl.formatMessage(messages.blocks)} to='/blocks' />
+        <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
         <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
       </div>
 
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
index 7548e6d56..4a0e7684d 100644
--- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -8,45 +8,49 @@ import {
   deleteFromTimelines
 } from '../../actions/timelines';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import createStream from '../../stream';
+
+const mapStateToProps = state => ({
+  accessToken: state.getIn(['meta', 'access_token'])
+});
 
 const HashtagTimeline = React.createClass({
 
   propTypes: {
     params: React.PropTypes.object.isRequired,
-    dispatch: React.PropTypes.func.isRequired
+    dispatch: React.PropTypes.func.isRequired,
+    accessToken: React.PropTypes.string.isRequired
   },
 
   mixins: [PureRenderMixin],
 
   _subscribe (dispatch, id) {
-    if (typeof App !== 'undefined') {
-      this.subscription = App.cable.subscriptions.create({
-        channel: 'HashtagChannel',
-        tag: id
-      }, {
-
-        received (data) {
-          switch(data.event) {
-          case 'update':
-            dispatch(updateTimeline('tag', JSON.parse(data.payload)));
-            break;
-          case 'delete':
-            dispatch(deleteFromTimelines(data.payload));
-            break;
-          }
+    const { accessToken } = this.props;
+
+    this.subscription = createStream(accessToken, `hashtag&tag=${id}`, {
+
+      received (data) {
+        switch(data.event) {
+        case 'update':
+          dispatch(updateTimeline('tag', JSON.parse(data.payload)));
+          break;
+        case 'delete':
+          dispatch(deleteFromTimelines(data.payload));
+          break;
         }
+      }
 
-      });
-    }
+    });
   },
 
   _unsubscribe () {
     if (typeof this.subscription !== 'undefined') {
-      this.subscription.unsubscribe();
+      this.subscription.close();
+      this.subscription = null;
     }
   },
 
-  componentWillMount () {
+  componentDidMount () {
     const { dispatch } = this.props;
     const { id } = this.props.params;
 
@@ -79,4 +83,4 @@ const HashtagTimeline = React.createClass({
 
 });
 
-export default connect()(HashtagTimeline);
+export default connect(mapStateToProps)(HashtagTimeline);
diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
new file mode 100644
index 000000000..d20a4d170
--- /dev/null
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -0,0 +1,21 @@
+const iconStyle = {
+  fontSize: '16px',
+  padding: '15px',
+  position: 'absolute',
+  right: '48px',
+  top: '0',
+  cursor: 'pointer',
+  background: '#2f3441'
+};
+
+const ClearColumnButton = ({ onClick }) => (
+  <div className='column-icon' style={iconStyle} onClick={onClick}>
+    <i className='fa fa-trash' />
+  </div>
+);
+
+ClearColumnButton.propTypes = {
+  onClick: React.PropTypes.func.isRequired
+};
+
+export default ClearColumnButton;
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index d3300acd5..6d10768de 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Column from '../ui/components/column';
-import { expandNotifications } from '../../actions/notifications';
+import { expandNotifications, clearNotifications } from '../../actions/notifications';
 import NotificationContainer from './containers/notification_container';
 import { ScrollContainer } from 'react-router-scroll';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -10,6 +10,7 @@ import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
 import Immutable from 'immutable';
 import LoadMore from '../../components/load_more';
+import ClearColumnButton from './components/clear_column_button';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' }
@@ -64,6 +65,10 @@ const Notifications = React.createClass({
     this.props.dispatch(expandNotifications());
   },
 
+  handleClear () {
+    this.props.dispatch(clearNotifications());
+  },
+
   setRef (c) {
     this.node = c;
   },
@@ -90,6 +95,7 @@ const Notifications = React.createClass({
       return (
         <Column icon='bell' heading={intl.formatMessage(messages.title)}>
           <ColumnSettingsContainer />
+          <ClearColumnButton onClick={this.handleClear} />
           <ScrollContainer scrollKey='notifications'>
             {scrollableArea}
           </ScrollContainer>
@@ -99,6 +105,7 @@ const Notifications = React.createClass({
       return (
         <Column icon='bell' heading={intl.formatMessage(messages.title)}>
           <ColumnSettingsContainer />
+          <ClearColumnButton onClick={this.handleClear} />
           {scrollableArea}
         </Column>
       );
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
index 42970061c..36d68dbbb 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -9,46 +9,51 @@ import {
 } from '../../actions/timelines';
 import { defineMessages, injectIntl } from 'react-intl';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import createStream from '../../stream';
 
 const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Public' }
 });
 
+const mapStateToProps = state => ({
+  accessToken: state.getIn(['meta', 'access_token'])
+});
+
 const PublicTimeline = React.createClass({
 
   propTypes: {
     dispatch: React.PropTypes.func.isRequired,
-    intl: React.PropTypes.object.isRequired
+    intl: React.PropTypes.object.isRequired,
+    accessToken: React.PropTypes.string.isRequired
   },
 
   mixins: [PureRenderMixin],
 
-  componentWillMount () {
-    const { dispatch } = this.props;
+  componentDidMount () {
+    const { dispatch, accessToken } = this.props;
 
     dispatch(refreshTimeline('public'));
 
-    if (typeof App !== 'undefined') {
-      this.subscription = App.cable.subscriptions.create('PublicChannel', {
+    this.subscription = createStream(accessToken, 'public', {
 
-        received (data) {
-          switch(data.event) {
-          case 'update':
-            dispatch(updateTimeline('public', JSON.parse(data.payload)));
-            break;
-          case 'delete':
-            dispatch(deleteFromTimelines(data.payload));
-            break;
-          }
+      received (data) {
+        switch(data.event) {
+        case 'update':
+          dispatch(updateTimeline('public', JSON.parse(data.payload)));
+          break;
+        case 'delete':
+          dispatch(deleteFromTimelines(data.payload));
+          break;
         }
+      }
 
-      });
-    }
+    });
   },
 
   componentWillUnmount () {
     if (typeof this.subscription !== 'undefined') {
-      this.subscription.unsubscribe();
+      this.subscription.close();
+      this.subscription = null;
     }
   },
 
@@ -65,4 +70,4 @@ const PublicTimeline = React.createClass({
 
 });
 
-export default connect()(injectIntl(PublicTimeline));
+export default connect(mapStateToProps)(injectIntl(PublicTimeline));
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 993c649d2..894fa3176 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -84,8 +84,8 @@ const Status = React.createClass({
     this.props.dispatch(mentionCompose(account, router));
   },
 
-  handleOpenMedia (url) {
-    this.props.dispatch(openMedia(url));
+  handleOpenMedia (media, index) {
+    this.props.dispatch(openMedia(media, index));
   },
 
   renderChildren (list) {
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
index c382e108d..2eafe5a8f 100644
--- a/app/assets/javascripts/components/features/ui/components/column.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -1,4 +1,4 @@
-import ColumnHeader    from './column_header';
+import ColumnHeader from './column_header';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 
 const easingOutQuint = (x, t, b, c, d) => c*((t=t/d-1)*t*t*t*t + 1) + b;
@@ -58,16 +58,18 @@ const Column = React.createClass({
   },
 
   render () {
+    const { heading, icon, children } = this.props;
+
     let header = '';
 
-    if (this.props.heading) {
-      header = <ColumnHeader icon={this.props.icon} type={this.props.heading} onClick={this.handleHeaderClick} />;
+    if (heading) {
+      header = <ColumnHeader icon={icon} type={heading} onClick={this.handleHeaderClick} />;
     }
 
     return (
       <div className='column' style={style} onWheel={this.handleWheel}>
         {header}
-        {this.props.children}
+        {children}
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
index 53d162462..334e5c199 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -1,12 +1,18 @@
 import { connect } from 'react-redux';
-import { closeModal } from '../../../actions/modal';
+import {
+  closeModal,
+  decreaseIndexInModal,
+  increaseIndexInModal
+} from '../../../actions/modal';
 import Lightbox from '../../../components/lightbox';
 import ImageLoader from 'react-imageloader';
 import LoadingIndicator from '../../../components/loading_indicator';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const mapStateToProps = state => ({
-  url: state.getIn(['modal', 'url']),
+  media: state.getIn(['modal', 'media']),
+  index: state.getIn(['modal', 'index']),
   isVisible: state.getIn(['modal', 'open'])
 });
 
@@ -17,6 +23,14 @@ const mapDispatchToProps = dispatch => ({
 
   onOverlayClicked () {
     dispatch(closeModal());
+  },
+
+  onNextClicked () {
+    dispatch(increaseIndexInModal());
+  },
+
+  onPrevClicked () {
+    dispatch(decreaseIndexInModal());
   }
 });
 
@@ -38,27 +52,115 @@ const preloader = () => (
   </div>
 );
 
+const leftNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  color: '#fff',
+  fontSize: '24px',
+  top: '0',
+  left: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
+const rightNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  color: '#fff',
+  fontSize: '24px',
+  top: '0',
+  right: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
 const Modal = React.createClass({
 
   propTypes: {
-    url: React.PropTypes.string,
+    media: ImmutablePropTypes.list,
+    index: React.PropTypes.number.isRequired,
     isVisible: React.PropTypes.bool,
     onCloseClicked: React.PropTypes.func,
-    onOverlayClicked: React.PropTypes.func
+    onOverlayClicked: React.PropTypes.func,
+    onNextClicked: React.PropTypes.func,
+    onPrevClicked: React.PropTypes.func
   },
 
   mixins: [PureRenderMixin],
 
+  handleNextClick () {
+    this.props.onNextClicked();
+  },
+
+  handlePrevClick () {
+    this.props.onPrevClicked();
+  },
+
+  componentDidMount () {
+    this._listener = e => {
+      if (!this.props.isVisible) {
+        return;
+      }
+
+      switch(e.key) {
+      case 'ArrowLeft':
+        this.props.onPrevClicked();
+        break;
+      case 'ArrowRight':
+        this.props.onNextClicked();
+        break;
+      }
+    };
+
+    window.addEventListener('keyup', this._listener);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this._listener);
+  },
+
   render () {
-    const { url, ...other } = this.props;
+    const { media, index, ...other } = this.props;
+
+    if (!media) {
+      return null;
+    }
+
+    const url      = media.get(index).get('url');
+    const hasLeft  = index > 0;
+    const hasRight = index + 1 < media.size;
+
+    let leftNav, rightNav;
+
+    leftNav = rightNav = '';
+
+    if (hasLeft) {
+      leftNav = <div style={leftNavStyle} onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
+    }
+
+    if (hasRight) {
+      rightNav = <div style={rightNavStyle} onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
+    }
 
     return (
       <Lightbox {...other}>
+        {leftNav}
+
         <ImageLoader
           src={url}
           preloader={preloader}
           imgProps={{ style: imageStyle }}
         />
+
+        {rightNav}
       </Lightbox>
     );
   }
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 8af7b0c3c..100989d22 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
@@ -7,8 +7,9 @@ import { createSelector } from 'reselect';
 const getStatusIds = createSelector([
   (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
   (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
-  (state)           => state.get('statuses')
-], (columnSettings, statusIds, statuses) => statusIds.filter(id => {
+  (state)           => state.get('statuses'),
+  (state)           => state.getIn(['meta', 'me'])
+], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
   const statusForId = statuses.get(id);
   let showStatus    = true;
 
@@ -17,7 +18,7 @@ const getStatusIds = createSelector([
   }
 
   if (columnSettings.getIn(['shows', 'reply']) === false) {
-    showStatus = showStatus && statusForId.get('in_reply_to_id') === null;
+    showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
   }
 
   if (columnSettings.getIn(['regex', 'body'], '').trim().length > 0) {