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.jsx8
-rw-r--r--app/assets/javascripts/components/features/account_timeline/index.jsx5
-rw-r--r--app/assets/javascripts/components/features/community_timeline/index.jsx75
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx78
-rw-r--r--app/assets/javascripts/components/features/compose/components/drawer.jsx4
-rw-r--r--app/assets/javascripts/components/features/compose/components/private_toggle.jsx27
-rw-r--r--app/assets/javascripts/components/features/compose/components/reply_indicator.jsx17
-rw-r--r--app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx31
-rw-r--r--app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx27
-rw-r--r--app/assets/javascripts/components/features/compose/components/unlisted_toggle.jsx32
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx117
-rw-r--r--app/assets/javascripts/components/features/compose/containers/private_toggle_container.jsx17
-rw-r--r--app/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx24
-rw-r--r--app/assets/javascripts/components/features/compose/containers/sensitive_toggle_container.jsx18
-rw-r--r--app/assets/javascripts/components/features/compose/containers/spoiler_toggle_container.jsx17
-rw-r--r--app/assets/javascripts/components/features/compose/containers/unlisted_toggle_container.jsx31
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx8
-rw-r--r--app/assets/javascripts/components/features/home_timeline/index.jsx16
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx2
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx25
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx14
-rw-r--r--app/assets/javascripts/components/features/ui/components/column.jsx7
-rw-r--r--app/assets/javascripts/components/features/ui/components/column_header.jsx7
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx11
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx24
26 files changed, 464 insertions, 182 deletions
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index a4f0ca768..e1aae3c77 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -1,7 +1,7 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
+import escapeTextContentForBrowser from 'escape-html';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import IconButton from '../../../components/icon_button';
 
@@ -14,7 +14,7 @@ const messages = defineMessages({
 const Header = React.createClass({
 
   propTypes: {
-    account: ImmutablePropTypes.map.isRequired,
+    account: ImmutablePropTypes.map,
     me: React.PropTypes.number.isRequired,
     onFollow: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired
@@ -25,6 +25,10 @@ const Header = React.createClass({
   render () {
     const { account, me, intl } = this.props;
 
+    if (!account) {
+      return null;
+    }
+
     let displayName = account.get('display_name');
     let info        = '';
     let actionBtn   = '';
diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx
index 349510295..f92e1b49c 100644
--- a/app/assets/javascripts/components/features/account_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/index.jsx
@@ -16,6 +16,7 @@ import Immutable from 'immutable';
 const mapStateToProps = (state, props) => ({
   statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()),
   isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']),
+  hasMore: !!state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'next']),
   me: state.getIn(['meta', 'me'])
 });
 
@@ -26,6 +27,7 @@ const AccountTimeline = React.createClass({
     dispatch: React.PropTypes.func.isRequired,
     statusIds: ImmutablePropTypes.list,
     isLoading: React.PropTypes.bool,
+    hasMore: React.PropTypes.bool,
     me: React.PropTypes.number.isRequired
   },
 
@@ -48,7 +50,7 @@ const AccountTimeline = React.createClass({
   },
 
   render () {
-    const { statusIds, isLoading, me } = this.props;
+    const { statusIds, isLoading, hasMore, me } = this.props;
 
     if (!statusIds && isLoading) {
       return (
@@ -66,6 +68,7 @@ const AccountTimeline = React.createClass({
           prepend={<HeaderContainer accountId={this.props.params.accountId} />}
           statusIds={statusIds}
           isLoading={isLoading}
+          hasMore={hasMore}
           me={me}
           onScrollToBottom={this.handleScrollToBottom}
         />
diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx
new file mode 100644
index 000000000..aa1b8368e
--- /dev/null
+++ b/app/assets/javascripts/components/features/community_timeline/index.jsx
@@ -0,0 +1,75 @@
+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,
+  deleteFromTimelines
+} from '../../actions/timelines';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import createStream from '../../stream';
+
+const messages = defineMessages({
+  title: { id: 'column.community', defaultMessage: 'Local' }
+});
+
+const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
+  accessToken: state.getIn(['meta', 'access_token'])
+});
+
+const CommunityTimeline = React.createClass({
+
+  propTypes: {
+    dispatch: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired,
+    accessToken: React.PropTypes.string.isRequired,
+    hasUnread: React.PropTypes.bool
+  },
+
+  mixins: [PureRenderMixin],
+
+  componentDidMount () {
+    const { dispatch, accessToken } = this.props;
+
+    dispatch(refreshTimeline('community'));
+
+    this.subscription = createStream(accessToken, 'public:local', {
+
+      received (data) {
+        switch(data.event) {
+        case 'update':
+          dispatch(updateTimeline('community', JSON.parse(data.payload)));
+          break;
+        case 'delete':
+          dispatch(deleteFromTimelines(data.payload));
+          break;
+        }
+      }
+
+    });
+  },
+
+  componentWillUnmount () {
+    if (typeof this.subscription !== 'undefined') {
+      this.subscription.close();
+      this.subscription = null;
+    }
+  },
+
+  render () {
+    const { intl, hasUnread } = this.props;
+
+    return (
+      <Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
+        <ColumnBackButtonSlim />
+        <StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
+      </Column>
+    );
+  },
+
+});
+
+export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
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 9edc01ed7..31ae8e034 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -2,7 +2,7 @@ import CharacterCounter from './character_counter';
 import Button from '../../../components/button';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import ReplyIndicator from './reply_indicator';
+import ReplyIndicatorContainer from '../containers/reply_indicator_container';
 import UploadButton from './upload_button';
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
 import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container';
@@ -11,6 +11,10 @@ import UploadButtonContainer from '../containers/upload_button_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
 import Collapsable from '../../../components/collapsable';
+import UnlistedToggleContainer from '../containers/unlisted_toggle_container';
+import SpoilerToggleContainer from '../containers/spoiler_toggle_container';
+import PrivateToggleContainer from '../containers/private_toggle_container';
+import SensitiveToggleContainer from '../containers/sensitive_toggle_container';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -25,30 +29,24 @@ const ComposeForm = React.createClass({
     text: React.PropTypes.string.isRequired,
     suggestion_token: React.PropTypes.string,
     suggestions: ImmutablePropTypes.list,
-    sensitive: React.PropTypes.bool,
     spoiler: React.PropTypes.bool,
-    spoiler_text: React.PropTypes.string,
-    unlisted: React.PropTypes.bool,
     private: React.PropTypes.bool,
+    unlisted: React.PropTypes.bool,
+    spoiler_text: React.PropTypes.string,
     fileDropDate: React.PropTypes.instanceOf(Date),
+    focusDate: React.PropTypes.instanceOf(Date),
+    preselectDate: React.PropTypes.instanceOf(Date),
     is_submitting: React.PropTypes.bool,
     is_uploading: React.PropTypes.bool,
-    in_reply_to: ImmutablePropTypes.map,
-    media_count: React.PropTypes.number,
     me: React.PropTypes.number,
     needsPrivacyWarning: React.PropTypes.bool,
     mentionedDomains: React.PropTypes.array.isRequired,
     onChange: React.PropTypes.func.isRequired,
     onSubmit: React.PropTypes.func.isRequired,
-    onCancelReply: React.PropTypes.func.isRequired,
     onClearSuggestions: React.PropTypes.func.isRequired,
     onFetchSuggestions: React.PropTypes.func.isRequired,
     onSuggestionSelected: React.PropTypes.func.isRequired,
-    onChangeSensitivity: React.PropTypes.func.isRequired,
-    onChangeSpoilerness: React.PropTypes.func.isRequired,
     onChangeSpoilerText: React.PropTypes.func.isRequired,
-    onChangeVisibility: React.PropTypes.func.isRequired,
-    onChangeListability: React.PropTypes.func.isRequired,
   },
 
   mixins: [PureRenderMixin],
@@ -80,34 +78,17 @@ const ComposeForm = React.createClass({
     this.props.onSuggestionSelected(tokenStart, token, value);
   },
 
-  handleChangeSensitivity (e) {
-    this.props.onChangeSensitivity(e.target.checked);
-  },
-
-  handleChangeSpoilerness (e) {
-    this.props.onChangeSpoilerness(e.target.checked);
-    this.props.onChangeSpoilerText('');
-  },
-
   handleChangeSpoilerText (e) {
     this.props.onChangeSpoilerText(e.target.value);
   },
 
-  handleChangeVisibility (e) {
-    this.props.onChangeVisibility(e.target.checked);
-  },
-
-  handleChangeListability (e) {
-    this.props.onChangeListability(e.target.checked);
-  },
-
   componentDidUpdate (prevProps) {
-    if ((prevProps.in_reply_to === null && this.props.in_reply_to !== null) || (prevProps.in_reply_to !== null && this.props.in_reply_to !== null && prevProps.in_reply_to.get('id') !== this.props.in_reply_to.get('id'))) {
+    if (this.props.focusDate !== prevProps.focusDate) {
       // If replying to zero or one users, places the cursor at the end of the textbox.
       // If replying to more than one user, selects any usernames past the first;
       // this provides a convenient shortcut to drop everyone else from the conversation.
-      const selectionStart = this.props.text.search(/\s/) + 1;
       const selectionEnd   = this.props.text.length;
+      const selectionStart = (this.props.preselectDate !== prevProps.preselectDate) ? (this.props.text.search(/\s/) + 1) : selectionEnd;
 
       this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
       this.autosuggestTextarea.textarea.focus();
@@ -122,14 +103,9 @@ const ComposeForm = React.createClass({
     const { intl, needsPrivacyWarning, mentionedDomains } = this.props;
     const disabled = this.props.is_submitting || this.props.is_uploading;
 
-    let replyArea      = '';
     let publishText    = '';
     let privacyWarning = '';
-    let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
-
-    if (this.props.in_reply_to) {
-      replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
-    }
+    let reply_to_other = false;
 
     if (needsPrivacyWarning) {
       privacyWarning = (
@@ -158,7 +134,8 @@ const ComposeForm = React.createClass({
         </Collapsable>
 
         {privacyWarning}
-        {replyArea}
+
+        <ReplyIndicatorContainer />
 
         <AutosuggestTextarea
           ref={this.setAutosuggestTextarea}
@@ -180,29 +157,10 @@ const ComposeForm = React.createClass({
           <UploadButtonContainer style={{ paddingTop: '4px' }} />
         </div>
 
-        <label className='compose-form__label with-border' style={{ marginTop: '10px' }}>
-          <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} />
-          <span className='compose-form__label__text'><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
-        </label>
-
-        <label className='compose-form__label with-border'>
-          <Toggle checked={this.props.private} onChange={this.handleChangeVisibility} />
-          <span className='compose-form__label__text'><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
-        </label>
-
-        <Collapsable isVisible={!(this.props.private || reply_to_other)} fullHeight={39.5}>
-          <label className='compose-form__label'>
-            <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
-            <span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display in public timeline' /></span>
-          </label>
-        </Collapsable>
-
-        <Collapsable isVisible={this.props.media_count > 0} fullHeight={39.5}>
-          <label className='compose-form__label'>
-            <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
-            <span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
-          </label>
-        </Collapsable>
+        <SpoilerToggleContainer />
+        <PrivateToggleContainer />
+        <UnlistedToggleContainer />
+        <SensitiveToggleContainer />
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/features/compose/components/drawer.jsx b/app/assets/javascripts/components/features/compose/components/drawer.jsx
index 83f3fa27d..ab67c86ea 100644
--- a/app/assets/javascripts/components/features/compose/components/drawer.jsx
+++ b/app/assets/javascripts/components/features/compose/components/drawer.jsx
@@ -3,7 +3,8 @@ import { injectIntl, defineMessages } from 'react-intl';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
-  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
+  public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+  community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
   logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
 });
@@ -15,6 +16,7 @@ const Drawer = ({ children, withHeader, intl }) => {
     header = (
       <div className='drawer__header'>
         <Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
+        <Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
         <Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
         <a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
         <a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
diff --git a/app/assets/javascripts/components/features/compose/components/private_toggle.jsx b/app/assets/javascripts/components/features/compose/components/private_toggle.jsx
new file mode 100644
index 000000000..902ee70ca
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/private_toggle.jsx
@@ -0,0 +1,27 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
+
+const PrivateToggle = React.createClass({
+
+  propTypes: {
+    isPrivate: React.PropTypes.bool,
+    onChange: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { isPrivate, onChange } = this.props;
+
+    return (
+      <label className='compose-form__label with-border'>
+        <Toggle checked={isPrivate} onChange={onChange} />
+        <span className='compose-form__label__text'><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
+      </label>
+    );
+  }
+
+});
+
+export default PrivateToggle;
diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
index 73e5ee99e..a72bd32c2 100644
--- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
+++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
@@ -17,7 +17,7 @@ const ReplyIndicator = React.createClass({
   },
 
   propTypes: {
-    status: ImmutablePropTypes.map.isRequired,
+    status: ImmutablePropTypes.map,
     onCancel: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired
   },
@@ -36,17 +36,22 @@ const ReplyIndicator = React.createClass({
   },
 
   render () {
-    const { intl } = this.props;
-    const content  = { __html: emojify(this.props.status.get('content')) };
+    const { status, intl } = this.props;
+
+    if (!status) {
+      return null;
+    }
+
+    const content  = { __html: emojify(status.get('content')) };
 
     return (
       <div className='reply-indicator'>
         <div style={{ overflow: 'hidden', marginBottom: '5px' }}>
           <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
 
-          <a href={this.props.status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
-            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div>
-            <DisplayName account={this.props.status.get('account')} />
+          <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
+            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
+            <DisplayName account={status.get('account')} />
           </a>
         </div>
 
diff --git a/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx b/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx
new file mode 100644
index 000000000..97cc9487e
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/sensitive_toggle.jsx
@@ -0,0 +1,31 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
+import Collapsable from '../../../components/collapsable';
+
+const SensitiveToggle = React.createClass({
+
+  propTypes: {
+    hasMedia: React.PropTypes.bool,
+    isSensitive: React.PropTypes.bool,
+    onChange: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { hasMedia, isSensitive, onChange } = this.props;
+
+    return (
+      <Collapsable isVisible={hasMedia} fullHeight={39.5}>
+        <label className='compose-form__label'>
+          <Toggle checked={isSensitive} onChange={onChange} />
+          <span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
+        </label>
+      </Collapsable>
+    );
+  }
+
+});
+
+export default SensitiveToggle;
diff --git a/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx b/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx
new file mode 100644
index 000000000..1c59e4393
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/spoiler_toggle.jsx
@@ -0,0 +1,27 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
+
+const SpoilerToggle = React.createClass({
+
+  propTypes: {
+    isSpoiler: React.PropTypes.bool,
+    onChange: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { isSpoiler, onChange } = this.props;
+
+    return (
+      <label className='compose-form__label with-border' style={{ marginTop: '10px' }}>
+        <Toggle checked={isSpoiler} onChange={onChange} />
+        <span className='compose-form__label__text'><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
+      </label>
+    );
+  }
+
+});
+
+export default SpoilerToggle;
diff --git a/app/assets/javascripts/components/features/compose/components/unlisted_toggle.jsx b/app/assets/javascripts/components/features/compose/components/unlisted_toggle.jsx
new file mode 100644
index 000000000..0745051eb
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/unlisted_toggle.jsx
@@ -0,0 +1,32 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { FormattedMessage } from 'react-intl';
+import Toggle from 'react-toggle';
+import Collapsable from '../../../components/collapsable';
+
+const UnlistedToggle = React.createClass({
+
+  propTypes: {
+    isPrivate: React.PropTypes.bool,
+    isUnlisted: React.PropTypes.bool,
+    isReplyToOther: React.PropTypes.bool,
+    onChangeListability: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { isPrivate, isUnlisted, isReplyToOther, onChangeListability } = this.props;
+
+    return (
+      <Collapsable isVisible={!(isPrivate || isReplyToOther)} fullHeight={39.5}>
+        <label className='compose-form__label'>
+          <Toggle checked={isUnlisted} onChange={onChangeListability} />
+          <span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display on public timelines' /></span>
+        </label>
+      </Collapsable>
+    );
+  }
+
+});
+
+export default UnlistedToggle;
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 2671ea618..53129af6e 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
@@ -1,95 +1,70 @@
 import { connect } from 'react-redux';
 import ComposeForm from '../components/compose_form';
+import { createSelector } from 'reselect';
 import {
   changeCompose,
   submitCompose,
-  cancelReplyCompose,
   clearComposeSuggestions,
   fetchComposeSuggestions,
   selectComposeSuggestion,
-  changeComposeSensitivity,
-  changeComposeSpoilerness,
   changeComposeSpoilerText,
-  changeComposeVisibility,
-  changeComposeListability
 } from '../../../actions/compose';
-import { makeGetStatus } from '../../../selectors';
 
-const makeMapStateToProps = () => {
-  const getStatus = makeGetStatus();
+const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
 
-  const mapStateToProps = function (state, props) {
-    const mentionedUsernamesWithDomains = state.getIn(['compose', 'text']).match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig);
+const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
+  return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
+});
 
-    return {
-      text: state.getIn(['compose', 'text']),
-      suggestion_token: state.getIn(['compose', 'suggestion_token']),
-      suggestions: state.getIn(['compose', 'suggestions']),
-      sensitive: state.getIn(['compose', 'sensitive']),
-      spoiler: state.getIn(['compose', 'spoiler']),
-      spoiler_text: state.getIn(['compose', 'spoiler_text']),
-      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']),
-      needsPrivacyWarning: state.getIn(['compose', 'private']) && mentionedUsernamesWithDomains !== null,
-      mentionedDomains: mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []
-    };
-  };
-
-  return mapStateToProps;
-};
+const mapStateToProps = (state, props) => {
+  const mentionedUsernames = getMentionedUsernames(state);
+  const mentionedUsernamesWithDomains = getMentionedDomains(state);
 
-const mapDispatchToProps = function (dispatch) {
   return {
-    onChange (text) {
-      dispatch(changeCompose(text));
-    },
-
-    onSubmit () {
-      dispatch(submitCompose());
-    },
-
-    onCancelReply () {
-      dispatch(cancelReplyCompose());
-    },
+    text: state.getIn(['compose', 'text']),
+    suggestion_token: state.getIn(['compose', 'suggestion_token']),
+    suggestions: state.getIn(['compose', 'suggestions']),
+    spoiler: state.getIn(['compose', 'spoiler']),
+    spoiler_text: state.getIn(['compose', 'spoiler_text']),
+    unlisted: state.getIn(['compose', 'unlisted'], ),
+    private: state.getIn(['compose', 'private']),
+    fileDropDate: state.getIn(['compose', 'fileDropDate']),
+    focusDate: state.getIn(['compose', 'focusDate']),
+    preselectDate: state.getIn(['compose', 'preselectDate']),
+    is_submitting: state.getIn(['compose', 'is_submitting']),
+    is_uploading: state.getIn(['compose', 'is_uploading']),
+    me: state.getIn(['compose', 'me']),
+    needsPrivacyWarning: state.getIn(['compose', 'private']) && mentionedUsernames !== null,
+    mentionedDomains: mentionedUsernamesWithDomains
+  };
+};
 
-    onClearSuggestions () {
-      dispatch(clearComposeSuggestions());
-    },
+const mapDispatchToProps = (dispatch) => ({
 
-    onFetchSuggestions (token) {
-      dispatch(fetchComposeSuggestions(token));
-    },
+  onChange (text) {
+    dispatch(changeCompose(text));
+  },
 
-    onSuggestionSelected (position, token, accountId) {
-      dispatch(selectComposeSuggestion(position, token, accountId));
-    },
+  onSubmit () {
+    dispatch(submitCompose());
+  },
 
-    onChangeSensitivity (checked) {
-      dispatch(changeComposeSensitivity(checked));
-    },
+  onClearSuggestions () {
+    dispatch(clearComposeSuggestions());
+  },
 
-    onChangeSpoilerness (checked) {
-      dispatch(changeComposeSpoilerness(checked));
-    },
+  onFetchSuggestions (token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
 
-    onChangeSpoilerText (checked) {
-      dispatch(changeComposeSpoilerText(checked));
-    },
+  onSuggestionSelected (position, token, accountId) {
+    dispatch(selectComposeSuggestion(position, token, accountId));
+  },
 
-    onChangeVisibility (checked) {
-      dispatch(changeComposeVisibility(checked));
-    },
+  onChangeSpoilerText (checked) {
+    dispatch(changeComposeSpoilerText(checked));
+  },
 
-    onChangeListability (checked) {
-      dispatch(changeComposeListability(checked));
-    }
-  }
-};
+});
 
-export default connect(makeMapStateToProps, mapDispatchToProps)(ComposeForm);
+export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
diff --git a/app/assets/javascripts/components/features/compose/containers/private_toggle_container.jsx b/app/assets/javascripts/components/features/compose/containers/private_toggle_container.jsx
new file mode 100644
index 000000000..ee3596902
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/private_toggle_container.jsx
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import PrivateToggle from '../components/private_toggle';
+import { changeComposeVisibility } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  isPrivate: state.getIn(['compose', 'private'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (e) {
+    dispatch(changeComposeVisibility(e.target.checked));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PrivateToggle);
diff --git a/app/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx b/app/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx
new file mode 100644
index 000000000..39b48f3b6
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/reply_indicator_container.jsx
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import { cancelReplyCompose } from '../../../actions/compose';
+import { makeGetStatus } from '../../../selectors';
+import ReplyIndicator from '../components/reply_indicator';
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = (state, props) => ({
+    status: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onCancel () {
+    dispatch(cancelReplyCompose());
+  }
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
diff --git a/app/assets/javascripts/components/features/compose/containers/sensitive_toggle_container.jsx b/app/assets/javascripts/components/features/compose/containers/sensitive_toggle_container.jsx
new file mode 100644
index 000000000..97b3361ba
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/sensitive_toggle_container.jsx
@@ -0,0 +1,18 @@
+import { connect } from 'react-redux';
+import SensitiveToggle from '../components/sensitive_toggle';
+import { changeComposeSensitivity } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  hasMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+  isSensitive: state.getIn(['compose', 'sensitive'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (e) {
+    dispatch(changeComposeSensitivity(e.target.checked));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SensitiveToggle);
diff --git a/app/assets/javascripts/components/features/compose/containers/spoiler_toggle_container.jsx b/app/assets/javascripts/components/features/compose/containers/spoiler_toggle_container.jsx
new file mode 100644
index 000000000..0bd4df759
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/spoiler_toggle_container.jsx
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import SpoilerToggle from '../components/spoiler_toggle';
+import { changeComposeSpoilerness } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  isSpoiler: state.getIn(['compose', 'spoiler'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (e) {
+    dispatch(changeComposeSpoilerness(e.target.checked));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SpoilerToggle);
diff --git a/app/assets/javascripts/components/features/compose/containers/unlisted_toggle_container.jsx b/app/assets/javascripts/components/features/compose/containers/unlisted_toggle_container.jsx
new file mode 100644
index 000000000..ceac903d9
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/unlisted_toggle_container.jsx
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux';
+import UnlistedToggle from '../components/unlisted_toggle';
+import { makeGetStatus } from '../../../selectors';
+import { changeComposeListability } from '../../../actions/compose';
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = state => {
+    const status = getStatus(state, state.getIn(['compose', 'in_reply_to']));
+    const me     = state.getIn(['compose', 'me']);
+
+    return {
+      isPrivate: state.getIn(['compose', 'private']),
+      isUnlisted: state.getIn(['compose', 'unlisted']),
+      isReplyToOther: status ? status.getIn(['account', 'id']) !== me : false
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+
+  onChangeListability (e) {
+    dispatch(changeComposeListability(e.target.checked));
+  }
+
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(UnlistedToggle);
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index af86919c1..f8433b8f4 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -7,7 +7,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
-  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
+  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Whole Known Network' },
+  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   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' },
@@ -30,6 +31,7 @@ const GettingStarted = ({ intl, me }) => {
   return (
     <Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
       <div style={{ position: 'relative' }}>
+        <ColumnLink icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />
         <ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
         <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
         <ColumnLink icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
index 6cb9e5482..7fb413336 100644
--- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -12,6 +12,7 @@ import { FormattedMessage } from 'react-intl';
 import createStream from '../../stream';
 
 const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
   accessToken: state.getIn(['meta', 'access_token'])
 });
 
@@ -20,7 +21,8 @@ const HashtagTimeline = React.createClass({
   propTypes: {
     params: React.PropTypes.object.isRequired,
     dispatch: React.PropTypes.func.isRequired,
-    accessToken: React.PropTypes.string.isRequired
+    accessToken: React.PropTypes.string.isRequired,
+    hasUnread: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
@@ -72,10 +74,10 @@ const HashtagTimeline = React.createClass({
   },
 
   render () {
-    const { id } = this.props.params;
+    const { id, hasUnread } = this.props.params;
 
     return (
-      <Column icon='hashtag' heading={id}>
+      <Column icon='hashtag' active={hasUnread} heading={id}>
         <ColumnBackButtonSlim />
         <StatusListContainer type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} />
       </Column>
diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx
index 23e198701..a2b775764 100644
--- a/app/assets/javascripts/components/features/home_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/home_timeline/index.jsx
@@ -1,3 +1,4 @@
+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';
@@ -9,25 +10,30 @@ const messages = defineMessages({
   title: { id: 'column.home', defaultMessage: 'Home' }
 });
 
+const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0
+});
+
 const HomeTimeline = React.createClass({
 
   propTypes: {
-    intl: React.PropTypes.object.isRequired
+    intl: React.PropTypes.object.isRequired,
+    hasUnread: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
 
   render () {
-    const { intl } = this.props;
+    const { intl, hasUnread } = this.props;
 
     return (
-      <Column icon='home' heading={intl.formatMessage(messages.title)}>
+      <Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}>
         <ColumnSettingsContainer />
-        <StatusListContainer {...this.props} type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren’t following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
+        <StatusListContainer {...this.props} type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} />
       </Column>
     );
   },
 
 });
 
-export default injectIntl(HomeTimeline);
+export default connect(mapStateToProps)(injectIntl(HomeTimeline));
diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx
index fa8466140..0de4df52e 100644
--- a/app/assets/javascripts/components/features/notifications/components/notification.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx
@@ -5,7 +5,7 @@ import AccountContainer from '../../../containers/account_container';
 import { FormattedMessage } from 'react-intl';
 import Permalink from '../../../components/permalink';
 import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
+import escapeTextContentForBrowser from 'escape-html';
 
 const linkStyle = {
   fontWeight: '500'
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 9532b8af8..0da3544f6 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, clearNotifications } from '../../actions/notifications';
+import { expandNotifications, clearNotifications, scrollTopNotifications } from '../../actions/notifications';
 import NotificationContainer from './containers/notification_container';
 import { ScrollContainer } from 'react-router-scroll';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -23,7 +23,8 @@ const getNotifications = createSelector([
 
 const mapStateToProps = state => ({
   notifications: getNotifications(state),
-  isLoading: state.getIn(['notifications', 'isLoading'], true)
+  isLoading: state.getIn(['notifications', 'isLoading'], true),
+  isUnread: state.getIn(['notifications', 'unread']) > 0
 });
 
 const Notifications = React.createClass({
@@ -33,7 +34,8 @@ const Notifications = React.createClass({
     dispatch: React.PropTypes.func.isRequired,
     trackScroll: React.PropTypes.bool,
     intl: React.PropTypes.object.isRequired,
-    isLoading: React.PropTypes.bool
+    isLoading: React.PropTypes.bool,
+    isUnread: React.PropTypes.bool
   },
 
   getDefaultProps () {
@@ -51,6 +53,10 @@ const Notifications = React.createClass({
 
     if (250 > offset && !this.props.isLoading) {
       this.props.dispatch(expandNotifications());
+    } else if (scrollTop < 100) {
+      this.props.dispatch(scrollTopNotifications(true));
+    } else {
+      this.props.dispatch(scrollTopNotifications(false));
     }
   },
 
@@ -74,18 +80,25 @@ const Notifications = React.createClass({
   },
 
   render () {
-    const { intl, notifications, trackScroll, isLoading } = this.props;
+    const { intl, notifications, trackScroll, isLoading, isUnread } = this.props;
 
     let loadMore       = '';
     let scrollableArea = '';
+    let unread         = '';
 
     if (!isLoading && notifications.size > 0) {
       loadMore = <LoadMore onClick={this.handleLoadMore} />;
     }
 
+    if (isUnread) {
+      unread = <div className='notifications__unread-indicator' />;
+    }
+
     if (isLoading || notifications.size > 0) {
       scrollableArea = (
         <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
+          {unread}
+
           <div>
             {notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
             {loadMore}
@@ -102,7 +115,7 @@ const Notifications = React.createClass({
 
     if (trackScroll) {
       return (
-        <Column icon='bell' heading={intl.formatMessage(messages.title)}>
+        <Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
           <ColumnSettingsContainer />
           <ClearColumnButton onClick={this.handleClear} />
           <ScrollContainer scrollKey='notifications'>
@@ -112,7 +125,7 @@ const Notifications = React.createClass({
       );
     } else {
       return (
-        <Column icon='bell' heading={intl.formatMessage(messages.title)}>
+        <Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
           <ColumnSettingsContainer />
           <ClearColumnButton onClick={this.handleClear} />
           {scrollableArea}
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
index 36d68dbbb..ce4eacc92 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -7,15 +7,16 @@ import {
   updateTimeline,
   deleteFromTimelines
 } from '../../actions/timelines';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
 import createStream from '../../stream';
 
 const messages = defineMessages({
-  title: { id: 'column.public', defaultMessage: 'Public' }
+  title: { id: 'column.public', defaultMessage: 'Whole Known Network' }
 });
 
 const mapStateToProps = state => ({
+  hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
   accessToken: state.getIn(['meta', 'access_token'])
 });
 
@@ -24,7 +25,8 @@ const PublicTimeline = React.createClass({
   propTypes: {
     dispatch: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired,
-    accessToken: React.PropTypes.string.isRequired
+    accessToken: React.PropTypes.string.isRequired,
+    hasUnread: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
@@ -58,12 +60,12 @@ const PublicTimeline = React.createClass({
   },
 
   render () {
-    const { intl } = this.props;
+    const { intl, hasUnread } = this.props;
 
     return (
-      <Column icon='globe' heading={intl.formatMessage(messages.title)}>
+      <Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}>
         <ColumnBackButtonSlim />
-        <StatusListContainer type='public' />
+        <StatusListContainer type='public' emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} />
       </Column>
     );
   },
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
index 5b0603ee9..2b7e11bf1 100644
--- a/app/assets/javascripts/components/features/ui/components/column.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -34,7 +34,8 @@ const Column = React.createClass({
   propTypes: {
     heading: React.PropTypes.string,
     icon: React.PropTypes.string,
-    children: React.PropTypes.node
+    children: React.PropTypes.node,
+    active: React.PropTypes.bool
   },
 
   mixins: [PureRenderMixin],
@@ -51,12 +52,12 @@ const Column = React.createClass({
   },
 
   render () {
-    const { heading, icon, children } = this.props;
+    const { heading, icon, children, active } = this.props;
 
     let header = '';
 
     if (heading) {
-      header = <ColumnHeader icon={icon} type={heading} onClick={this.handleHeaderClick} />;
+      header = <ColumnHeader icon={icon} active={active} type={heading} onClick={this.handleHeaderClick} />;
     }
 
     return (
diff --git a/app/assets/javascripts/components/features/ui/components/column_header.jsx b/app/assets/javascripts/components/features/ui/components/column_header.jsx
index 8b072d723..de55fa748 100644
--- a/app/assets/javascripts/components/features/ui/components/column_header.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column_header.jsx
@@ -5,6 +5,7 @@ const ColumnHeader = React.createClass({
   propTypes: {
     icon: React.PropTypes.string,
     type: React.PropTypes.string,
+    active: React.PropTypes.bool,
     onClick: React.PropTypes.func
   },
 
@@ -15,6 +16,8 @@ const ColumnHeader = React.createClass({
   },
 
   render () {
+    const { type, active } = this.props;
+
     let icon = '';
 
     if (this.props.icon) {
@@ -22,9 +25,9 @@ const ColumnHeader = React.createClass({
     }
 
     return (
-      <div className='column-header' onClick={this.handleClick}>
+      <div className={`column-header ${active ? 'active' : ''}`} onClick={this.handleClick}>
         {icon}
-        {this.props.type}
+        {type}
       </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 4c47fb8c5..d8301b20f 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -131,19 +131,14 @@ const Modal = React.createClass({
       return null;
     }
 
-    const url      = media.get(index).get('url');
-    const hasLeft  = index > 0;
-    const hasRight = index + 1 < media.size;
+    const url = media.get(index).get('url');
 
     let leftNav, rightNav;
 
     leftNav = rightNav = '';
 
-    if (hasLeft) {
-      leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
-    }
-
-    if (hasRight) {
+    if (media.size > 1) {
+      leftNav  = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
       rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
     }
 
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 100989d22..f249240d8 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
@@ -3,8 +3,9 @@ import StatusList from '../../../components/status_list';
 import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
 import Immutable from 'immutable';
 import { createSelector } from 'reselect';
+import { debounce } from 'react-decoration';
 
-const getStatusIds = createSelector([
+const makeGetStatusIds = () => createSelector([
   (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
   (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
   (state)           => state.get('statuses'),
@@ -33,26 +34,37 @@ const getStatusIds = createSelector([
   return showStatus;
 }));
 
-const mapStateToProps = (state, props) => ({
-  statusIds: getStatusIds(state, props),
-  isLoading: state.getIn(['timelines', props.type, 'isLoading'], true)
-});
+const makeMapStateToProps = () => {
+  const getStatusIds = makeGetStatusIds();
+
+  const mapStateToProps = (state, props) => ({
+    statusIds: getStatusIds(state, props),
+    isLoading: state.getIn(['timelines', props.type, 'isLoading'], true),
+    isUnread: state.getIn(['timelines', props.type, 'unread']) > 0,
+    hasMore: !!state.getIn(['timelines', props.type, 'next'])
+  });
+
+  return mapStateToProps;
+};
 
 const mapDispatchToProps = (dispatch, { type, id }) => ({
 
+  @debounce(300, true)
   onScrollToBottom () {
     dispatch(scrollTopTimeline(type, false));
     dispatch(expandTimeline(type, id));
   },
 
+  @debounce(100)
   onScrollToTop () {
     dispatch(scrollTopTimeline(type, true));
   },
 
+  @debounce(100)
   onScroll () {
     dispatch(scrollTopTimeline(type, false));
   }
 
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(StatusList);
+export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList);