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/action_bar.jsx48
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx61
-rw-r--r--app/assets/javascripts/components/features/account_timeline/components/header.jsx22
-rw-r--r--app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx17
-rw-r--r--app/assets/javascripts/components/features/account_timeline/index.jsx5
-rw-r--r--app/assets/javascripts/components/features/community_timeline/index.jsx95
-rw-r--r--app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx7
-rw-r--r--app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx15
-rw-r--r--app/assets/javascripts/components/features/compose/components/character_counter.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx198
-rw-r--r--app/assets/javascripts/components/features/compose/components/drawer.jsx42
-rw-r--r--app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx58
-rw-r--r--app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx101
-rw-r--r--app/assets/javascripts/components/features/compose/components/reply_indicator.jsx17
-rw-r--r--app/assets/javascripts/components/features/compose/components/search.jsx110
-rw-r--r--app/assets/javascripts/components/features/compose/components/search_results.jsx68
-rw-r--r--app/assets/javascripts/components/features/compose/components/text_icon_button.jsx31
-rw-r--r--app/assets/javascripts/components/features/compose/components/upload_button.jsx7
-rw-r--r--app/assets/javascripts/components/features/compose/components/upload_form.jsx28
-rw-r--r--app/assets/javascripts/components/features/compose/components/upload_progress.jsx44
-rw-r--r--app/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx15
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx117
-rw-r--r--app/assets/javascripts/components/features/compose/containers/privacy_dropdown_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/search_container.jsx20
-rw-r--r--app/assets/javascripts/components/features/compose/containers/search_results_container.jsx8
-rw-r--r--app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx49
-rw-r--r--app/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx24
-rw-r--r--app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx9
-rw-r--r--app/assets/javascripts/components/features/compose/index.jsx65
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx7
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx11
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx11
-rw-r--r--app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx12
-rw-r--r--app/assets/javascripts/components/features/home_timeline/index.jsx19
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx4
-rw-r--r--app/assets/javascripts/components/features/notifications/components/column_settings.jsx12
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx22
-rw-r--r--app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx5
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx59
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx46
-rw-r--r--app/assets/javascripts/components/features/report/components/status_check_box.jsx42
-rw-r--r--app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx19
-rw-r--r--app/assets/javascripts/components/features/report/index.jsx130
-rw-r--r--app/assets/javascripts/components/features/status/components/action_bar.jsx17
-rw-r--r--app/assets/javascripts/components/features/status/components/card.jsx43
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx6
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx65
-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/components/column_link.jsx1
-rw-r--r--app/assets/javascripts/components/features/ui/components/media_modal.jsx133
-rw-r--r--app/assets/javascripts/components/features/ui/components/modal_root.jsx80
-rw-r--r--app/assets/javascripts/components/features/ui/components/tabs_bar.jsx28
-rw-r--r--app/assets/javascripts/components/features/ui/components/upload_area.jsx32
-rw-r--r--app/assets/javascripts/components/features/ui/containers/modal_container.jsx166
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx24
-rw-r--r--app/assets/javascripts/components/features/ui/index.jsx71
58 files changed, 1698 insertions, 705 deletions
diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx
index fe110954d..80a32d3e2 100644
--- a/app/assets/javascripts/components/features/account/components/action_bar.jsx
+++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx
@@ -5,13 +5,16 @@ import { Link } from 'react-router';
 import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
 
 const messages = defineMessages({
-  mention: { id: 'account.mention', defaultMessage: 'Mention' },
+  mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
   edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
-  unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
+  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
-  block: { id: 'account.block', defaultMessage: 'Block' },
+  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+  block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+  mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
-  block: { id: 'account.block', defaultMessage: 'Block' }
+  report: { id: 'account.report', defaultMessage: 'Report @{name}' },
+  disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }
 });
 
 const outerDropdownStyle = {
@@ -32,7 +35,10 @@ const ActionBar = React.createClass({
     me: React.PropTypes.number.isRequired,
     onFollow: React.PropTypes.func,
     onBlock: React.PropTypes.func.isRequired,
-    onMention: React.PropTypes.func.isRequired
+    onMention: React.PropTypes.func.isRequired,
+    onReport: React.PropTypes.func.isRequired,
+    onMute: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -41,17 +47,31 @@ const ActionBar = React.createClass({
     const { account, me, intl } = this.props;
 
     let menu = [];
+    let extraInfo = '';
 
-    menu.push({ text: intl.formatMessage(messages.mention), action: this.props.onMention });
+    menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
+    menu.push(null);
 
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
-    } else if (account.getIn(['relationship', 'blocking'])) {
-      menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock });
-    } else if (account.getIn(['relationship', 'following'])) {
-      menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
     } else {
-      menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
+      if (account.getIn(['relationship', 'muting'])) {
+        menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
+      } else {
+        menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
+      }
+
+      if (account.getIn(['relationship', 'blocking'])) {
+        menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
+      } else {
+        menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
+      }
+
+      menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
+    }
+
+    if (account.get('acct') !== account.get('username')) {
+      extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>;
     }
 
     return (
@@ -63,17 +83,17 @@ const ActionBar = React.createClass({
         <div style={outerLinksStyle}>
           <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
             <span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
-            <strong><FormattedNumber value={account.get('statuses_count')} /></strong>
+            <strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong>
           </Link>
 
           <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
             <span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span>
-            <strong><FormattedNumber value={account.get('following_count')} /></strong>
+            <strong><FormattedNumber value={account.get('following_count')} /> {extraInfo}</strong>
           </Link>
 
           <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
             <span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
-            <strong><FormattedNumber value={account.get('followers_count')} /></strong>
+            <strong><FormattedNumber value={account.get('followers_count')} /> {extraInfo}</strong>
           </Link>
         </div>
       </div>
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index b2d943c1c..a359963c4 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -1,9 +1,10 @@
 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';
+import { Motion, spring } from 'react-motion';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -11,10 +12,51 @@ const messages = defineMessages({
   requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
 });
 
+const Avatar = React.createClass({
+
+  propTypes: {
+    account: ImmutablePropTypes.map.isRequired
+  },
+
+  getInitialState () {
+    return {
+      isHovered: false
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleMouseOver () {
+    if (this.state.isHovered) return;
+    this.setState({ isHovered: true });
+  },
+
+  handleMouseOut () {
+    if (!this.state.isHovered) return;
+    this.setState({ isHovered: false });
+  },
+
+  render () {
+    const { account }   = this.props;
+    const { isHovered } = this.state;
+
+    return (
+      <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
+        {({ radius }) =>
+          <a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
+            <img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
+          </a>
+        }
+      </Motion>
+    );
+  }
+
+});
+
 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 +67,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   = '';
@@ -35,7 +81,7 @@ const Header = React.createClass({
     }
 
     if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
-      info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
+      info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
     }
 
     if (me !== account.get('id')) {
@@ -64,14 +110,9 @@ const Header = React.createClass({
     return (
       <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
         <div style={{ padding: '20px 10px' }}>
-          <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
-            <div className='account__header__avatar' style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
-              <img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} />
-            </div>
-
-            <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
-          </a>
+          <Avatar account={account} />
 
+          <span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
           <span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
           <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
 
diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
index ff3e8af2d..99a10562e 100644
--- a/app/assets/javascripts/components/features/account_timeline/components/header.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
@@ -2,6 +2,7 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import InnerHeader from '../../account/components/header';
 import ActionBar from '../../account/components/action_bar';
+import MissingIndicator from '../../../components/missing_indicator';
 
 const Header = React.createClass({
   contextTypes: {
@@ -9,11 +10,13 @@ const Header = React.createClass({
   },
 
   propTypes: {
-    account: ImmutablePropTypes.map.isRequired,
+    account: ImmutablePropTypes.map,
     me: React.PropTypes.number.isRequired,
     onFollow: React.PropTypes.func.isRequired,
     onBlock: React.PropTypes.func.isRequired,
-    onMention: React.PropTypes.func.isRequired
+    onMention: React.PropTypes.func.isRequired,
+    onReport: React.PropTypes.func.isRequired,
+    onMute: React.PropTypes.func.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -30,11 +33,20 @@ const Header = React.createClass({
     this.props.onMention(this.props.account, this.context.router);
   },
 
+  handleReport () {
+    this.props.onReport(this.props.account);
+    this.context.router.push('/report');
+  },
+
+  handleMute() {
+    this.props.onMute(this.props.account);
+  },
+
   render () {
     const { account, me } = this.props;
 
-    if (!account) {
-      return null;
+    if (account === null) {
+      return <MissingIndicator />;
     }
 
     return (
@@ -50,6 +62,8 @@ const Header = React.createClass({
           me={me}
           onBlock={this.handleBlock}
           onMention={this.handleMention}
+          onReport={this.handleReport}
+          onMute={this.handleMute}
         />
       </div>
     );
diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
index dca826596..8472d25a5 100644
--- a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
@@ -5,9 +5,12 @@ import {
   followAccount,
   unfollowAccount,
   blockAccount,
-  unblockAccount
+  unblockAccount,
+  muteAccount,
+  unmuteAccount
 } from '../../../actions/accounts';
 import { mentionCompose } from '../../../actions/compose';
+import { initReport } from '../../../actions/reports';
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
@@ -39,6 +42,18 @@ const mapDispatchToProps = dispatch => ({
 
   onMention (account, router) {
     dispatch(mentionCompose(account, router));
+  },
+
+  onReport (account) {
+    dispatch(initReport(account));
+  },
+
+  onMute (account) {
+    if (account.getIn(['relationship', 'muting'])) {
+      dispatch(unmuteAccount(account.get('id')));
+    } else {
+      dispatch(muteAccount(account.get('id')));
+    }
   }
 });
 
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..0957338cf
--- /dev/null
+++ b/app/assets/javascripts/components/features/community_timeline/index.jsx
@@ -0,0 +1,95 @@
+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,
+  connectTimeline,
+  disconnectTimeline
+} 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'])
+});
+
+let subscription;
+
+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'));
+
+    if (typeof subscription !== 'undefined') {
+      return;
+    }
+
+    subscription = createStream(accessToken, 'public:local', {
+
+      connected () {
+        dispatch(connectTimeline('community'));
+      },
+
+      reconnected () {
+        dispatch(connectTimeline('community'));
+      },
+
+      disconnected () {
+        dispatch(disconnectTimeline('community'));
+      },
+
+      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 subscription !== 'undefined') {
+    //   subscription.close();
+    //   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/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
index 9ea7f190f..5591b45cf 100644
--- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
+++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
@@ -1,11 +1,16 @@
 import Avatar from '../../../components/avatar';
 import DisplayName from '../../../components/display_name';
+import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const AutosuggestAccount = ({ account }) => (
-  <div style={{ overflow: 'hidden' }}>
+  <div style={{ overflow: 'hidden' }} className='autosuggest-account'>
     <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
     <DisplayName account={account} />
   </div>
 );
 
+AutosuggestAccount.propTypes = {
+  account: ImmutablePropTypes.map.isRequired
+};
+
 export default AutosuggestAccount;
diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx
new file mode 100644
index 000000000..086488649
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/autosuggest_status.jsx
@@ -0,0 +1,15 @@
+import { FormattedMessage } from 'react-intl';
+import DisplayName from '../../../components/display_name';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+
+const AutosuggestStatus = ({ status }) => (
+  <div style={{ overflow: 'hidden' }} className='autosuggest-status'>
+    <FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} />
+  </div>
+);
+
+AutosuggestStatus.propTypes = {
+  status: ImmutablePropTypes.map.isRequired
+};
+
+export default AutosuggestStatus;
diff --git a/app/assets/javascripts/components/features/compose/components/character_counter.jsx b/app/assets/javascripts/components/features/compose/components/character_counter.jsx
index f0c1b7c8d..e6b675354 100644
--- a/app/assets/javascripts/components/features/compose/components/character_counter.jsx
+++ b/app/assets/javascripts/components/features/compose/components/character_counter.jsx
@@ -10,7 +10,7 @@ const CharacterCounter = React.createClass({
   mixins: [PureRenderMixin],
 
   render () {
-    const diff = this.props.max - this.props.text.length;
+    const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length;
 
     return (
       <span style={{ fontSize: '16px', cursor: 'default' }}>
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 46b62964a..b016d3f28 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -2,15 +2,19 @@ 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 UploadButton from './upload_button';
+import ReplyIndicatorContainer from '../containers/reply_indicator_container';
 import AutosuggestTextarea from '../../../components/autosuggest_textarea';
-import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container';
 import { debounce } from 'react-decoration';
 import UploadButtonContainer from '../containers/upload_button_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
-import { Motion, spring } from 'react-motion';
+import Collapsable from '../../../components/collapsable';
+import SpoilerButtonContainer from '../containers/spoiler_button_container';
+import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
+import SensitiveButtonContainer from '../containers/sensitive_button_container';
+import EmojiPickerDropdown from './emoji_picker_dropdown';
+import UploadFormContainer from '../containers/upload_form_container';
+import TextIconButton from './text_icon_button';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -25,28 +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,
+    privacy: React.PropTypes.string,
     spoiler_text: React.PropTypes.string,
-    unlisted: React.PropTypes.bool,
-    private: React.PropTypes.bool,
-    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,
+    onPaste: React.PropTypes.func.isRequired,
+    onPickEmoji: React.PropTypes.func.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -75,37 +75,31 @@ const ComposeForm = React.createClass({
   },
 
   onSuggestionSelected (tokenStart, token, value) {
+    this._restoreCaret = null;
     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;
+      let selectionEnd, selectionStart;
+
+      if (this.props.preselectDate !== prevProps.preselectDate) {
+        selectionEnd   = this.props.text.length;
+        selectionStart = this.props.text.search(/\s/) + 1;
+      } else if (typeof this._restoreCaret === 'number') {
+        selectionStart = this._restoreCaret;
+        selectionEnd   = this._restoreCaret;
+      } else {
+        selectionEnd   = this.props.text.length;
+        selectionStart = selectionEnd;
+      }
 
       this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
       this.autosuggestTextarea.textarea.focus();
@@ -116,83 +110,85 @@ const ComposeForm = React.createClass({
     this.autosuggestTextarea = c;
   },
 
-  render () {
-    const { intl }  = this.props;
-    let replyArea   = '';
-    let publishText = '';
-    const disabled  = this.props.is_submitting || this.props.is_uploading;
+  handleEmojiPick (data) {
+    const position     = this.autosuggestTextarea.textarea.selectionStart;
+    this._restoreCaret = position + data.shortname.length + 1;
+    this.props.onPickEmoji(position, data);
+  },
 
-    if (this.props.in_reply_to) {
-      replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
+  render () {
+    const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
+    const disabled = this.props.is_submitting || this.props.is_uploading;
+
+    let publishText    = '';
+    let privacyWarning = '';
+    let reply_to_other = false;
+
+    if (needsPrivacyWarning) {
+      privacyWarning = (
+        <div className='compose-form__warning'>
+          <FormattedMessage
+            id='compose_form.privacy_disclaimer'
+            defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
+            values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
+          />
+        </div>
+      );
     }
 
-    let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
-
-    if (this.props.private) {
+    if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
       publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
     } else {
-      publishText = intl.formatMessage(messages.publish) + (!this.props.unlisted ? '!' : '');
+      publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== '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) }}>
-          {({ opacity, height }) =>
-            <div className="spoiler-input" style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" />
-            </div>
-          }
-        </Motion>
-
-        {replyArea}
-
-        <AutosuggestTextarea
-          ref={this.setAutosuggestTextarea}
-          placeholder={intl.formatMessage(messages.placeholder)}
-          disabled={disabled}
-          fileDropDate={this.props.fileDropDate}
-          value={this.props.text}
-          onChange={this.handleChange}
-          suggestions={this.props.suggestions}
-          onKeyDown={this.handleKeyDown}
-          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
-          onSuggestionsClearRequested={this.onSuggestionsClearRequested}
-          onSuggestionSelected={this.onSuggestionSelected}
-        />
-
-        <div style={{ marginTop: '10px', overflow: 'hidden' }}>
-          <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' }} />
+        <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
+          <div className="spoiler-input">
+            <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" />
+          </div>
+        </Collapsable>
+
+        {privacyWarning}
+
+        <ReplyIndicatorContainer />
+
+        <div style={{ position: 'relative' }}>
+          <AutosuggestTextarea
+            ref={this.setAutosuggestTextarea}
+            placeholder={intl.formatMessage(messages.placeholder)}
+            disabled={disabled}
+            value={this.props.text}
+            onChange={this.handleChange}
+            suggestions={this.props.suggestions}
+            onKeyDown={this.handleKeyDown}
+            onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+            onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+            onSuggestionSelected={this.onSuggestionSelected}
+            onPaste={onPaste}
+          />
+
+          <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
+        </div>
+
+        <div className='compose-form__modifiers'>
+          <UploadFormContainer />
         </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>
-
-        <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) }}>
-          {({ opacity, height }) =>
-            <label className='compose-form__label' style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <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>
-          }
-        </Motion>
-
-        <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(this.props.media_count === 0 ? 0 : 100), height: spring(this.props.media_count === 0 ? 0 : 39.5) }}>
-          {({ opacity, height }) =>
-            <label className='compose-form__label' style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <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>
-          }
-        </Motion>
+        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+          <div className='compose-form__buttons'>
+            <UploadButtonContainer />
+            <PrivacyDropdownContainer />
+            <SensitiveButtonContainer />
+            <SpoilerButtonContainer />
+          </div>
+
+          <div style={{ display: 'flex' }}>
+            <div style={{ paddingTop: '10px', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
+            <div style={{ paddingTop: '10px' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
+          </div>
+        </div>
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/features/compose/components/drawer.jsx b/app/assets/javascripts/components/features/compose/components/drawer.jsx
deleted file mode 100644
index 83f3fa27d..000000000
--- a/app/assets/javascripts/components/features/compose/components/drawer.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Link } from 'react-router';
-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' },
-  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
-  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
-});
-
-const Drawer = ({ children, withHeader, intl }) => {
-  let header = '';
-
-  if (withHeader) {
-    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.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>
-      </div>
-    );
-  }
-
-  return (
-    <div className='drawer'>
-      {header}
-
-      <div className='drawer__inner'>
-        {children}
-      </div>
-    </div>
-  );
-};
-
-Drawer.propTypes = {
-  withHeader: React.PropTypes.bool,
-  children: React.PropTypes.node,
-  intl: React.PropTypes.object
-};
-
-export default injectIntl(Drawer);
diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
new file mode 100644
index 000000000..1920b29bf
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
@@ -0,0 +1,58 @@
+import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
+import EmojiPicker from 'emojione-picker';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }
+});
+
+const settings = {
+  imageType: 'png',
+  sprites: false,
+  imagePathPNG: '/emoji/'
+};
+
+const style = {
+  position: 'absolute',
+  right: '5px',
+  top: '5px'
+};
+
+const EmojiPickerDropdown = React.createClass({
+
+  propTypes: {
+    intl: React.PropTypes.object.isRequired,
+    onPickEmoji: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  setRef (c) {
+    this.dropdown = c;
+  },
+
+  handleChange (data) {
+    this.dropdown.hide();
+    this.props.onPickEmoji(data);
+  },
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Dropdown ref={this.setRef} style={style}>
+        <DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
+          <img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
+        </DropdownTrigger>
+
+        <DropdownContent className='dropdown__left'>
+          <EmojiPicker emojione={settings} onChange={this.handleChange} />
+        </DropdownContent>
+      </Dropdown>
+    );
+  }
+
+});
+
+export default injectIntl(EmojiPickerDropdown);
diff --git a/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx
new file mode 100644
index 000000000..e54fa4d28
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/privacy_dropdown.jsx
@@ -0,0 +1,101 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { injectIntl, defineMessages } from 'react-intl';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
+  public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
+  unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
+  unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
+  private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
+  private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
+  direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
+  direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
+  change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }
+});
+
+const iconStyle = {
+  lineHeight: '27px',
+  height: null
+};
+
+const PrivacyDropdown = React.createClass({
+
+  propTypes: {
+    value: React.PropTypes.string.isRequired,
+    onChange: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  getInitialState () {
+    return {
+      open: false
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleToggle () {
+    this.setState({ open: !this.state.open });
+  },
+
+  handleClick (value, e) {
+    e.preventDefault();
+    this.setState({ open: false });
+    this.props.onChange(value);
+  },
+
+  onGlobalClick (e) {
+    if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
+      this.setState({ open: false });
+    }
+  },
+
+  componentDidMount () {
+    window.addEventListener('click', this.onGlobalClick);
+    window.addEventListener('touchstart', this.onGlobalClick);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('click', this.onGlobalClick);
+    window.removeEventListener('touchstart', this.onGlobalClick);
+  },
+
+  setRef (c) {
+    this.node = c;
+  },
+
+  render () {
+    const { value, onChange, intl } = this.props;
+    const { open } = this.state;
+
+    const options = [
+      { icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) },
+      { icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) },
+      { icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) },
+      { icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) }
+    ];
+
+    const valueOption = options.find(item => item.value === value);
+
+    return (
+      <div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
+        <div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
+        <div className='privacy-dropdown__dropdown'>
+          {options.map(item =>
+            <div key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
+              <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
+              <div className='privacy-dropdown__option__content'>
+                <strong>{item.shortText}</strong>
+                {item.longText}
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default injectIntl(PrivacyDropdown);
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/search.jsx b/app/assets/javascripts/components/features/compose/components/search.jsx
index c1f23939d..936e003f2 100644
--- a/app/assets/javascripts/components/features/compose/components/search.jsx
+++ b/app/assets/javascripts/components/features/compose/components/search.jsx
@@ -1,118 +1,68 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Autosuggest from 'react-autosuggest';
-import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
-import { debounce } from 'react-decoration';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 
 const messages = defineMessages({
   placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
 });
 
-const getSuggestionValue = suggestion => suggestion.value;
-
-const renderSuggestion = suggestion => {
-  if (suggestion.type === 'account') {
-    return <AutosuggestAccountContainer id={suggestion.id} />;
-  } else {
-    return <span>#{suggestion.id}</span>
-  }
-};
-
-const renderSectionTitle = section => (
-  <strong><FormattedMessage id={`search.${section.title}`} defaultMessage={section.title} /></strong>
-);
-
-const getSectionSuggestions = section => section.items;
-
-const outerStyle = {
-  padding: '10px',
-  lineHeight: '20px',
-  position: 'relative'
-};
-
-const iconStyle = {
-  position: 'absolute',
-  top: '18px',
-  right: '20px',
-  fontSize: '18px',
-  pointerEvents: 'none'
-};
-
 const Search = React.createClass({
 
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
   propTypes: {
-    suggestions: React.PropTypes.array.isRequired,
     value: React.PropTypes.string.isRequired,
+    submitted: React.PropTypes.bool,
     onChange: React.PropTypes.func.isRequired,
+    onSubmit: React.PropTypes.func.isRequired,
     onClear: React.PropTypes.func.isRequired,
-    onFetch: React.PropTypes.func.isRequired,
-    onReset: React.PropTypes.func.isRequired,
+    onShow: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired
   },
 
   mixins: [PureRenderMixin],
 
-  onChange (_, { newValue }) {
-    if (typeof newValue !== 'string') {
-      return;
-    }
-
-    this.props.onChange(newValue);
+  handleChange (e) {
+    this.props.onChange(e.target.value);
   },
 
-  onSuggestionsClearRequested () {
+  handleClear (e) {
+    e.preventDefault();
     this.props.onClear();
   },
 
-  @debounce(500)
-  onSuggestionsFetchRequested ({ value }) {
-    value = value.replace('#', '');
-    this.props.onFetch(value.trim());
+  handleKeyDown (e) {
+    if (e.key === 'Enter') {
+      e.preventDefault();
+      this.props.onSubmit();
+    }
   },
 
-  onSuggestionSelected (_, { suggestion }) {
-    if (suggestion.type === 'account') {
-      this.context.router.push(`/accounts/${suggestion.id}`);
-    } else {
-      this.context.router.push(`/timelines/tag/${suggestion.id}`);
-    }
+  handleFocus () {
+    this.props.onShow();
   },
 
   render () {
-    const inputProps = {
-      placeholder: this.props.intl.formatMessage(messages.placeholder),
-      value: this.props.value,
-      onChange: this.onChange,
-      className: 'search__input'
-    };
+    const { intl, value, submitted } = this.props;
+    const hasValue = value.length > 0 || submitted;
 
     return (
-      <div className='search' style={outerStyle}>
-        <Autosuggest
-          multiSection={true}
-          suggestions={this.props.suggestions}
-          focusFirstSuggestion={true}
-          focusInputOnSuggestionClick={false}
-          alwaysRenderSuggestions={false}
-          onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
-          onSuggestionsClearRequested={this.onSuggestionsClearRequested}
-          onSuggestionSelected={this.onSuggestionSelected}
-          getSuggestionValue={getSuggestionValue}
-          renderSuggestion={renderSuggestion}
-          renderSectionTitle={renderSectionTitle}
-          getSectionSuggestions={getSectionSuggestions}
-          inputProps={inputProps}
+      <div className='search'>
+        <input
+          className='search__input'
+          type='text'
+          placeholder={intl.formatMessage(messages.placeholder)}
+          value={value}
+          onChange={this.handleChange}
+          onKeyUp={this.handleKeyDown}
+          onFocus={this.handleFocus}
         />
 
-        <div style={iconStyle}><i className='fa fa-search' /></div>
+        <div className='search__icon'>
+          <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
+          <i className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} onClick={this.handleClear} />
+        </div>
       </div>
     );
-  },
+  }
 
 });
 
diff --git a/app/assets/javascripts/components/features/compose/components/search_results.jsx b/app/assets/javascripts/components/features/compose/components/search_results.jsx
new file mode 100644
index 000000000..fd05e7f7e
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/search_results.jsx
@@ -0,0 +1,68 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import AccountContainer from '../../../containers/account_container';
+import StatusContainer from '../../../containers/status_container';
+import { Link } from 'react-router';
+
+const SearchResults = React.createClass({
+
+  propTypes: {
+    results: ImmutablePropTypes.map.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { results } = this.props;
+
+    let accounts, statuses, hashtags;
+    let count = 0;
+
+    if (results.get('accounts') && results.get('accounts').size > 0) {
+      count   += results.get('accounts').size;
+      accounts = (
+        <div className='search-results__section'>
+          {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
+        </div>
+      );
+    }
+
+    if (results.get('statuses') && results.get('statuses').size > 0) {
+      count   += results.get('statuses').size;
+      statuses = (
+        <div className='search-results__section'>
+          {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
+        </div>
+      );
+    }
+
+    if (results.get('hashtags') && results.get('hashtags').size > 0) {
+      count += results.get('hashtags').size;
+      hashtags = (
+        <div className='search-results__section'>
+          {results.get('hashtags').map(hashtag =>
+            <Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
+              #{hashtag}
+            </Link>
+          )}
+        </div>
+      );
+    }
+
+    return (
+      <div className='search-results'>
+        <div className='search-results__header'>
+          <FormattedMessage id='search_results.total' defaultMessage='{count} {count, plural, one {result} other {results}}' values={{ count }} />
+        </div>
+
+        {accounts}
+        {statuses}
+        {hashtags}
+      </div>
+    );
+  }
+
+});
+
+export default SearchResults;
diff --git a/app/assets/javascripts/components/features/compose/components/text_icon_button.jsx b/app/assets/javascripts/components/features/compose/components/text_icon_button.jsx
new file mode 100644
index 000000000..e3ac63d87
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/text_icon_button.jsx
@@ -0,0 +1,31 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+
+const TextIconButton = React.createClass({
+
+  propTypes: {
+    label: React.PropTypes.string.isRequired,
+    title: React.PropTypes.string,
+    active: React.PropTypes.bool,
+    onClick: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleClick (e) {
+    e.preventDefault();
+    this.props.onClick();
+  },
+
+  render () {
+    const { label, title, active } = this.props;
+
+    return (
+      <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} onClick={this.handleClick}>
+        {label}
+      </button>
+    );
+  }
+
+});
+
+export default TextIconButton;
diff --git a/app/assets/javascripts/components/features/compose/components/upload_button.jsx b/app/assets/javascripts/components/features/compose/components/upload_button.jsx
index 4c8181aa1..2ba0e8fd2 100644
--- a/app/assets/javascripts/components/features/compose/components/upload_button.jsx
+++ b/app/assets/javascripts/components/features/compose/components/upload_button.jsx
@@ -6,6 +6,11 @@ const messages = defineMessages({
   upload: { id: 'upload_button.label', defaultMessage: 'Add media' }
 });
 
+const iconStyle = {
+  lineHeight: '27px',
+  height: null
+};
+
 const UploadButton = React.createClass({
 
   propTypes: {
@@ -37,7 +42,7 @@ const UploadButton = React.createClass({
 
     return (
       <div style={this.props.style}>
-        <IconButton icon='photo' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} size={24} />
+        <IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} style={iconStyle} size={18} inverted />
         <input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
       </div>
     );
diff --git a/app/assets/javascripts/components/features/compose/components/upload_form.jsx b/app/assets/javascripts/components/features/compose/components/upload_form.jsx
index 94c94b4b7..77590d90d 100644
--- a/app/assets/javascripts/components/features/compose/components/upload_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/upload_form.jsx
@@ -2,6 +2,8 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import IconButton from '../../../components/icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
+import UploadProgressContainer from '../containers/upload_progress_container';
+import { Motion, spring } from 'react-motion';
 
 const messages = defineMessages({
   undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }
@@ -11,7 +13,6 @@ const UploadForm = React.createClass({
 
   propTypes: {
     media: ImmutablePropTypes.list.isRequired,
-    is_uploading: React.PropTypes.bool,
     onRemoveFile: React.PropTypes.func.isRequired,
     intl: React.PropTypes.object.isRequired
   },
@@ -21,21 +22,22 @@ const UploadForm = React.createClass({
   render () {
     const { intl, media } = this.props;
 
-    if (!media.size) {
-      return null;
-    }
-
-    const uploads = media.map(attachment => (
-      <div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'>
-        <div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
-          <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
-        </div>
+    const uploads = media.map(attachment =>
+      <div key={attachment.get('id')} style={{ margin: '5px', flex: '1 1 0' }}>
+        <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
+          {({ scale }) =>
+            <div style={{ transform: `translateZ(0) scale(${scale})`, width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
+              <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
+            </div>
+          }
+        </Motion>
       </div>
-    ));
+    );
 
     return (
-      <div style={{ marginBottom: '20px', padding: '10px', overflow: 'hidden', flexShrink: '0' }}>
-        {uploads}
+      <div style={{ overflow: 'hidden' }}>
+        <UploadProgressContainer />
+        <div style={{ display: 'flex', padding: '5px' }}>{uploads}</div>
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/features/compose/components/upload_progress.jsx b/app/assets/javascripts/components/features/compose/components/upload_progress.jsx
new file mode 100644
index 000000000..86ffbf936
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/components/upload_progress.jsx
@@ -0,0 +1,44 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { Motion, spring } from 'react-motion';
+import { FormattedMessage } from 'react-intl';
+
+const UploadProgress = React.createClass({
+
+  propTypes: {
+    active: React.PropTypes.bool,
+    progress: React.PropTypes.number
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { active, progress } = this.props;
+
+    if (!active) {
+      return null;
+    }
+
+    return (
+      <div className='upload-progress'>
+        <div>
+          <i className='fa fa-upload' />
+        </div>
+
+        <div style={{ flex: '1 1 auto' }}>
+          <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
+
+          <div className='upload-progress__backdrop'>
+            <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
+              {({ width }) =>
+                <div className='upload-progress__tracker' style={{ width: `${width}%` }} />
+              }
+            </Motion>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default UploadProgress;
diff --git a/app/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx b/app/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx
new file mode 100644
index 000000000..ef46eb09c
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/autosuggest_status_container.jsx
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import AutosuggestStatus from '../components/autosuggest_status';
+import { makeGetStatus } from '../../../selectors';
+
+const makeMapStateToProps = () => {
+  const getStatus = makeGetStatus();
+
+  const mapStateToProps = (state, { id }) => ({
+    status: getStatus(state, id)
+  });
+
+  return mapStateToProps;
+};
+
+export default connect(makeMapStateToProps)(AutosuggestStatus);
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 c027875cd..604e1182f 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,91 +1,78 @@
 import { connect } from 'react-redux';
 import ComposeForm from '../components/compose_form';
+import { uploadCompose } from '../../../actions/compose';
+import { createSelector } from 'reselect';
 import {
   changeCompose,
   submitCompose,
-  cancelReplyCompose,
   clearComposeSuggestions,
   fetchComposeSuggestions,
   selectComposeSuggestion,
-  changeComposeSensitivity,
-  changeComposeSpoilerness,
   changeComposeSpoilerText,
-  changeComposeVisibility,
-  changeComposeListability
+  insertEmojiCompose
 } 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) {
-    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']),
-    };
-  };
+const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
+  return 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));
-    },
+    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']),
+    privacy: state.getIn(['compose', 'privacy']),
+    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', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
+    mentionedDomains: mentionedUsernamesWithDomains
+  };
+};
 
-    onSubmit () {
-      dispatch(submitCompose());
-    },
+const mapDispatchToProps = (dispatch) => ({
 
-    onCancelReply () {
-      dispatch(cancelReplyCompose());
-    },
+  onChange (text) {
+    dispatch(changeCompose(text));
+  },
 
-    onClearSuggestions () {
-      dispatch(clearComposeSuggestions());
-    },
+  onSubmit () {
+    dispatch(submitCompose());
+  },
 
-    onFetchSuggestions (token) {
-      dispatch(fetchComposeSuggestions(token));
-    },
+  onClearSuggestions () {
+    dispatch(clearComposeSuggestions());
+  },
 
-    onSuggestionSelected (position, token, accountId) {
-      dispatch(selectComposeSuggestion(position, token, accountId));
-    },
+  onFetchSuggestions (token) {
+    dispatch(fetchComposeSuggestions(token));
+  },
 
-    onChangeSensitivity (checked) {
-      dispatch(changeComposeSensitivity(checked));
-    },
+  onSuggestionSelected (position, token, accountId) {
+    dispatch(selectComposeSuggestion(position, token, accountId));
+  },
 
-    onChangeSpoilerness (checked) {
-      dispatch(changeComposeSpoilerness(checked));
-    },
+  onChangeSpoilerText (checked) {
+    dispatch(changeComposeSpoilerText(checked));
+  },
 
-    onChangeSpoilerText (checked) {
-      dispatch(changeComposeSpoilerText(checked));
-    },
+  onPaste (files) {
+    dispatch(uploadCompose(files));
+  },
 
-    onChangeVisibility (checked) {
-      dispatch(changeComposeVisibility(checked));
-    },
+  onPickEmoji (position, data) {
+    dispatch(insertEmojiCompose(position, data));
+  },
 
-    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/privacy_dropdown_container.jsx b/app/assets/javascripts/components/features/compose/containers/privacy_dropdown_container.jsx
new file mode 100644
index 000000000..1eee8f84c
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/privacy_dropdown_container.jsx
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import PrivacyDropdown from '../components/privacy_dropdown';
+import { changeComposeVisibility } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  value: state.getIn(['compose', 'privacy'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (value) {
+    dispatch(changeComposeVisibility(value));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
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/search_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
index 17a68f2fc..906c0c28c 100644
--- a/app/assets/javascripts/components/features/compose/containers/search_container.jsx
+++ b/app/assets/javascripts/components/features/compose/containers/search_container.jsx
@@ -1,15 +1,15 @@
 import { connect } from 'react-redux';
 import {
   changeSearch,
-  clearSearchSuggestions,
-  fetchSearchSuggestions,
-  resetSearch
+  clearSearch,
+  submitSearch,
+  showSearch
 } from '../../../actions/search';
 import Search from '../components/search';
 
 const mapStateToProps = state => ({
-  suggestions: state.getIn(['search', 'suggestions']),
-  value: state.getIn(['search', 'value'])
+  value: state.getIn(['search', 'value']),
+  submitted: state.getIn(['search', 'submitted'])
 });
 
 const mapDispatchToProps = dispatch => ({
@@ -19,15 +19,15 @@ const mapDispatchToProps = dispatch => ({
   },
 
   onClear () {
-    dispatch(clearSearchSuggestions());
+    dispatch(clearSearch());
   },
 
-  onFetch (value) {
-    dispatch(fetchSearchSuggestions(value));
+  onSubmit () {
+    dispatch(submitSearch());
   },
 
-  onReset () {
-    dispatch(resetSearch());
+  onShow () {
+    dispatch(showSearch());
   }
 
 });
diff --git a/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx
new file mode 100644
index 000000000..e5911fd38
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/search_results_container.jsx
@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import SearchResults from '../components/search_results';
+
+const mapStateToProps = state => ({
+  results: state.getIn(['search', 'results'])
+});
+
+export default connect(mapStateToProps)(SearchResults);
diff --git a/app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx b/app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx
new file mode 100644
index 000000000..074b568f4
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/sensitive_button_container.jsx
@@ -0,0 +1,49 @@
+import { connect } from 'react-redux';
+import TextIconButton from '../components/text_icon_button';
+import { changeComposeSensitivity } from '../../../actions/compose';
+import { Motion, spring } from 'react-motion';
+import { injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' }
+});
+
+const mapStateToProps = state => ({
+  visible: state.getIn(['compose', 'media_attachments']).size > 0,
+  active: state.getIn(['compose', 'sensitive'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onClick () {
+    dispatch(changeComposeSensitivity());
+  }
+
+});
+
+const SensitiveButton = React.createClass({
+
+  propTypes: {
+    visible: React.PropTypes.bool,
+    active: React.PropTypes.bool,
+    onClick: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  render () {
+    const { visible, active, onClick, intl } = this.props;
+
+    return (
+      <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
+        {({ scale }) =>
+          <div style={{ display: visible ? 'block' : 'none', transform: `translateZ(0) scale(${scale})` }}>
+            <TextIconButton onClick={onClick} label='NSFW' title={intl.formatMessage(messages.title)} active={active} />
+          </div>
+        }
+      </Motion>
+    );
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
diff --git a/app/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx b/app/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx
new file mode 100644
index 000000000..61ac32b85
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/spoiler_button_container.jsx
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux';
+import TextIconButton from '../components/text_icon_button';
+import { changeComposeSpoilerness } from '../../../actions/compose';
+import { injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind content warning' }
+});
+
+const mapStateToProps = (state, { intl }) => ({
+  label: 'CW',
+  title: intl.formatMessage(messages.title),
+  active: state.getIn(['compose', 'spoiler'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onClick () {
+    dispatch(changeComposeSpoilerness());
+  }
+
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
diff --git a/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx b/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx
new file mode 100644
index 000000000..b0f1d4d19
--- /dev/null
+++ b/app/assets/javascripts/components/features/compose/containers/upload_progress_container.jsx
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import UploadProgress from '../components/upload_progress';
+
+const mapStateToProps = (state, props) => ({
+  active: state.getIn(['compose', 'is_uploading']),
+  progress: state.getIn(['compose', 'progress'])
+});
+
+export default connect(mapStateToProps)(UploadProgress);
diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx
index f6095c0c6..9421de3ff 100644
--- a/app/assets/javascripts/components/features/compose/index.jsx
+++ b/app/assets/javascripts/components/features/compose/index.jsx
@@ -1,17 +1,34 @@
-import Drawer from './components/drawer';
 import ComposeFormContainer from './containers/compose_form_container';
 import UploadFormContainer from './containers/upload_form_container';
 import NavigationContainer from './containers/navigation_container';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
-import SearchContainer from './containers/search_container';
 import { connect } from 'react-redux';
 import { mountCompose, unmountCompose } from '../../actions/compose';
+import { Link } from 'react-router';
+import { injectIntl, defineMessages } from 'react-intl';
+import SearchContainer from './containers/search_container';
+import { Motion, spring } from 'react-motion';
+import SearchResultsContainer from './containers/search_results_container';
+
+const messages = defineMessages({
+  start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+  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' }
+});
+
+const mapStateToProps = state => ({
+  showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
+});
 
 const Compose = React.createClass({
 
   propTypes: {
     dispatch: React.PropTypes.func.isRequired,
-    withHeader: React.PropTypes.bool
+    withHeader: React.PropTypes.bool,
+    showSearch: React.PropTypes.bool,
+    intl: React.PropTypes.object.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -25,16 +42,46 @@ const Compose = React.createClass({
   },
 
   render () {
+    const { withHeader, showSearch, intl } = this.props;
+
+    let header = '';
+
+    if (withHeader) {
+      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>
+        </div>
+      );
+    }
+
     return (
-      <Drawer withHeader={this.props.withHeader}>
+      <div className='drawer'>
+        {header}
+
         <SearchContainer />
-        <NavigationContainer />
-        <ComposeFormContainer />
-        <UploadFormContainer />
-      </Drawer>
+
+        <div className='drawer__pager'>
+          <div className='drawer__inner'>
+            <NavigationContainer />
+            <ComposeFormContainer />
+          </div>
+
+          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+            {({ x }) =>
+              <div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                <SearchResultsContainer />
+              </div>
+            }
+          </Motion>
+        </div>
+      </div>
     );
   }
 
 });
 
-export default connect()(Compose);
+export default connect(mapStateToProps)(injectIntl(Compose));
diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
index 0d41d192f..1766655c2 100644
--- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -16,11 +16,8 @@ const outerStyle = {
 };
 
 const panelStyle = {
-  background: '#2f3441',
   display: 'flex',
   flexDirection: 'row',
-  borderTop: '1px solid #363c4b',
-  borderBottom: '1px solid #363c4b',
   padding: '10px 0'
 };
 
@@ -40,10 +37,10 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
           <DisplayName account={account} />
         </Permalink>
 
-        <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
+        <div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
       </div>
 
-      <div style={panelStyle}>
+      <div className='account--panel' style={panelStyle}>
         <div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
         <div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
       </div>
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index 0e1937b43..d7a78d9cc 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' />
@@ -39,12 +41,9 @@ const GettingStarted = ({ intl, me }) => {
         <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
       </div>
 
-      <div className='scrollable optionally-scrollable'>
+      <div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
         <div className='static-content getting-started'>
-          <p><FormattedMessage id='getting_started.about_addressing' defaultMessage='You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.' /></p>
-          <p><FormattedMessage id='getting_started.about_shortcuts' defaultMessage='If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.' /></p>
-          <p><FormattedMessage id='getting_started.about_developer' defaultMessage='The developer of this project can be followed as Gargron@mastodon.social' /></p>
-          <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on github at {github}' values={{ github: <a href="https://github.com/tootsuite/mastodon">tootsuite/mastodon</a> }} /></p>
+          <p><FormattedMessage id='getting_started.open_source_notice' defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}. {apps}.' values={{ github: <a href="https://github.com/tootsuite/mastodon" target="_blank">tootsuite/mastodon</a>, apps: <a href="https://github.com/tootsuite/mastodon/blob/master/docs/Using-Mastodon/Apps.md" target="_blank"><FormattedMessage id='getting_started.apps' defaultMessage='Various apps are available' /></a> }} /></p>
         </div>
       </div>
     </Column>
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
index 4a0e7684d..7fb413336 100644
--- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -8,9 +8,11 @@ import {
   deleteFromTimelines
 } from '../../actions/timelines';
 import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+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'])
 });
 
@@ -19,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],
@@ -71,12 +74,12 @@ 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} />
+        <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/components/column_settings.jsx b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
index 714be309b..92e700874 100644
--- a/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/home_timeline/components/column_settings.jsx
@@ -6,11 +6,10 @@ import SettingToggle from '../../notifications/components/setting_toggle';
 import SettingText from './setting_text';
 
 const messages = defineMessages({
-  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter by regular expressions' }
+  filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }
 });
 
 const outerStyle = {
-  background: '#373b4a',
   padding: '15px'
 };
 
@@ -18,7 +17,6 @@ const sectionStyle = {
   cursor: 'default',
   display: 'block',
   fontWeight: '500',
-  color: '#9baec8',
   marginBottom: '10px'
 };
 
@@ -42,18 +40,18 @@ const ColumnSettings = React.createClass({
 
     return (
       <ColumnCollapsable icon='sliders' fullHeight={209} onCollapse={onSave}>
-        <div style={outerStyle}>
-          <span style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
+        <div className='column-settings--outer' style={outerStyle}>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
 
           <div style={rowStyle}>
-            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reblogs' />} />
+            <SettingToggle settings={settings} settingKey={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
           </div>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 
           <div style={rowStyle}>
             <SettingText settings={settings} settingKey={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx
index 5d2263f15..a2b775764 100644
--- a/app/assets/javascripts/components/features/home_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/home_timeline/index.jsx
@@ -1,32 +1,39 @@
+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 { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
+import { Link } from 'react-router';
 
 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' />
+        <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/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
index d20a4d170..6aa9d1efa 100644
--- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -5,11 +5,11 @@ const iconStyle = {
   right: '48px',
   top: '0',
   cursor: 'pointer',
-  background: '#2f3441'
+  zIndex: '2'
 };
 
 const ClearColumnButton = ({ onClick }) => (
-  <div className='column-icon' style={iconStyle} onClick={onClick}>
+  <div className='column-icon' tabindex='0' style={iconStyle} onClick={onClick}>
     <i className='fa fa-trash' />
   </div>
 );
diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
index b63c1881a..f1b8ef57f 100644
--- a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
@@ -5,7 +5,6 @@ import ColumnCollapsable from '../../../components/column_collapsable';
 import SettingToggle from './setting_toggle';
 
 const outerStyle = {
-  background: '#373b4a',
   padding: '15px'
 };
 
@@ -13,7 +12,6 @@ const sectionStyle = {
   cursor: 'default',
   display: 'block',
   fontWeight: '500',
-  color: '#9baec8',
   marginBottom: '10px'
 };
 
@@ -40,8 +38,8 @@ const ColumnSettings = React.createClass({
 
     return (
       <ColumnCollapsable icon='sliders' fullHeight={616} onCollapse={onSave}>
-        <div style={outerStyle}>
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
+        <div className='column-settings--outer' style={outerStyle}>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'follow']} onChange={onChange} label={alertStr} />
@@ -49,7 +47,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'follow']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
@@ -57,7 +55,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'mention']} onChange={onChange} label={alertStr} />
@@ -65,7 +63,7 @@ const ColumnSettings = React.createClass({
             <SettingToggle settings={settings} settingKey={['sounds', 'mention']} onChange={onChange} label={soundStr} />
           </div>
 
-          <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
+          <span className='column-settings--section' style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
 
           <div style={rowStyle}>
             <SettingToggle settings={settings} settingKey={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx
index 140ba9134..0de4df52e 100644
--- a/app/assets/javascripts/components/features/notifications/components/notification.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx
@@ -5,17 +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';
-
-const messageStyle = {
-  marginLeft: '68px',
-  padding: '8px 0',
-  paddingBottom: '0',
-  cursor: 'default',
-  color: '#d9e1e8',
-  fontSize: '15px',
-  position: 'relative'
-};
+import escapeTextContentForBrowser from 'escape-html';
 
 const linkStyle = {
   fontWeight: '500'
@@ -32,9 +22,9 @@ const Notification = React.createClass({
   renderFollow (account, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
-            <i className='fa fa-fw fa-user-plus' style={{ color: '#2b90d9' }} />
+            <i className='fa fa-fw fa-user-plus' />
           </div>
 
           <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
@@ -52,7 +42,7 @@ const Notification = React.createClass({
   renderFavourite (notification, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
           </div>
@@ -68,9 +58,9 @@ const Notification = React.createClass({
   renderReblog (notification, link) {
     return (
       <div className='notification'>
-        <div style={messageStyle}>
+        <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
-            <i className='fa fa-fw fa-retweet' style={{ color: '#2b90d9' }} />
+            <i className='fa fa-fw fa-retweet' />
           </div>
 
           <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
diff --git a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
index c2438f716..eae3c2be2 100644
--- a/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/setting_toggle.jsx
@@ -11,14 +11,13 @@ const labelSpanStyle = {
   display: 'inline-block',
   verticalAlign: 'middle',
   marginBottom: '14px',
-  marginLeft: '8px',
-  color: '#9baec8'
+  marginLeft: '8px'
 };
 
 const SettingToggle = ({ settings, settingKey, label, onChange }) => (
   <label style={labelStyle}>
     <Toggle checked={settings.getIn(settingKey)} onChange={(e) => onChange(settingKey, e.target.checked)} />
-    <span style={labelSpanStyle}>{label}</span>
+    <span className='setting-toggle' style={labelSpanStyle}>{label}</span>
   </label>
 );
 
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 6d10768de..74b914ffd 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -2,10 +2,10 @@ 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 } from 'react-intl';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
 import Immutable from 'immutable';
@@ -13,7 +13,8 @@ import LoadMore from '../../components/load_more';
 import ClearColumnButton from './components/clear_column_button';
 
 const messages = defineMessages({
-  title: { id: 'column.notifications', defaultMessage: 'Notifications' }
+  title: { id: 'column.notifications', defaultMessage: 'Notifications' },
+  confirm: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to clear all your notifications?' }
 });
 
 const getNotifications = createSelector([
@@ -23,7 +24,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 +35,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 +54,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));
     }
   },
 
@@ -66,7 +73,9 @@ const Notifications = React.createClass({
   },
 
   handleClear () {
-    this.props.dispatch(clearNotifications());
+    if (window.confirm(this.props.intl.formatMessage(messages.confirm))) {
+      this.props.dispatch(clearNotifications());
+    }
   },
 
   setRef (c) {
@@ -74,26 +83,42 @@ const Notifications = React.createClass({
   },
 
   render () {
-    const { intl, notifications, trackScroll, isLoading } = this.props;
+    const { intl, notifications, trackScroll, isLoading, isUnread } = this.props;
 
-    let loadMore = '';
+    let loadMore       = '';
+    let scrollableArea = '';
+    let unread         = '';
 
     if (!isLoading && notifications.size > 0) {
       loadMore = <LoadMore onClick={this.handleLoadMore} />;
     }
 
-    const scrollableArea = (
-      <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
-        <div>
-          {notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
-          {loadMore}
+    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}
+          </div>
         </div>
-      </div>
-    );
+      );
+    } else {
+      scrollableArea = (
+        <div className='empty-column-indicator' ref={this.setRef}>
+          <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
+        </div>
+      );
+    }
 
     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'>
@@ -103,7 +128,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..6d766a83b 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -5,26 +5,32 @@ import Column from '../ui/components/column';
 import {
   refreshTimeline,
   updateTimeline,
-  deleteFromTimelines
+  deleteFromTimelines,
+  connectTimeline,
+  disconnectTimeline
 } 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'])
 });
 
+let subscription;
+
 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],
@@ -34,7 +40,23 @@ const PublicTimeline = React.createClass({
 
     dispatch(refreshTimeline('public'));
 
-    this.subscription = createStream(accessToken, 'public', {
+    if (typeof subscription !== 'undefined') {
+      return;
+    }
+
+    subscription = createStream(accessToken, 'public', {
+
+      connected () {
+        dispatch(connectTimeline('public'));
+      },
+
+      reconnected () {
+        dispatch(connectTimeline('public'));
+      },
+
+      disconnected () {
+        dispatch(disconnectTimeline('public'));
+      },
 
       received (data) {
         switch(data.event) {
@@ -51,19 +73,19 @@ const PublicTimeline = React.createClass({
   },
 
   componentWillUnmount () {
-    if (typeof this.subscription !== 'undefined') {
-      this.subscription.close();
-      this.subscription = null;
-    }
+    // if (typeof subscription !== 'undefined') {
+    //   subscription.close();
+    //   subscription = null;
+    // }
   },
 
   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/report/components/status_check_box.jsx b/app/assets/javascripts/components/features/report/components/status_check_box.jsx
new file mode 100644
index 000000000..6d976582b
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/components/status_check_box.jsx
@@ -0,0 +1,42 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import emojify from '../../../emoji';
+import Toggle from 'react-toggle';
+
+const StatusCheckBox = React.createClass({
+
+  propTypes: {
+    status: ImmutablePropTypes.map.isRequired,
+    checked: React.PropTypes.bool,
+    onToggle: React.PropTypes.func.isRequired,
+    disabled: React.PropTypes.bool
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { status, checked, onToggle, disabled } = this.props;
+    const content = { __html: emojify(status.get('content')) };
+
+    if (status.get('reblog')) {
+      return null;
+    }
+
+    return (
+      <div className='status-check-box' style={{ display: 'flex' }}>
+        <div
+          className='status__content'
+          style={{ flex: '1 1 auto', padding: '10px' }}
+          dangerouslySetInnerHTML={content}
+        />
+
+        <div style={{ flex: '0 0 auto', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
+          <Toggle checked={checked} onChange={onToggle} disabled={disabled} />
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default StatusCheckBox;
diff --git a/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx b/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx
new file mode 100644
index 000000000..67ce9d9f3
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+import StatusCheckBox from '../components/status_check_box';
+import { toggleStatusReport } from '../../../actions/reports';
+import Immutable from 'immutable';
+
+const mapStateToProps = (state, { id }) => ({
+  status: state.getIn(['statuses', id]),
+  checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id)
+});
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+
+  onToggle (e) {
+    dispatch(toggleStatusReport(id, e.target.checked));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
diff --git a/app/assets/javascripts/components/features/report/index.jsx b/app/assets/javascripts/components/features/report/index.jsx
new file mode 100644
index 000000000..3177d28b1
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/index.jsx
@@ -0,0 +1,130 @@
+import { connect } from 'react-redux';
+import { cancelReport, changeReportComment, submitReport } from '../../actions/reports';
+import { fetchAccountTimeline } from '../../actions/accounts';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Column from '../ui/components/column';
+import Button from '../../components/button';
+import { makeGetAccount } from '../../selectors';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import StatusCheckBox from './containers/status_check_box_container';
+import Immutable from 'immutable';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+
+const messages = defineMessages({
+  heading: { id: 'report.heading', defaultMessage: 'New report' },
+  placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
+  submit: { id: 'report.submit', defaultMessage: 'Submit' }
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = state => {
+    const accountId = state.getIn(['reports', 'new', 'account_id']);
+
+    return {
+      isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
+      account: getAccount(state, accountId),
+      comment: state.getIn(['reports', 'new', 'comment']),
+      statusIds: Immutable.OrderedSet(state.getIn(['timelines', 'accounts_timelines', accountId, 'items'])).union(state.getIn(['reports', 'new', 'status_ids']))
+    };
+  };
+
+  return mapStateToProps;
+};
+
+const textareaStyle = {
+  marginBottom: '10px'
+};
+
+const Report = React.createClass({
+
+  contextTypes: {
+    router: React.PropTypes.object
+  },
+
+  propTypes: {
+    isSubmitting: React.PropTypes.bool,
+    account: ImmutablePropTypes.map,
+    statusIds: ImmutablePropTypes.list.isRequired,
+    comment: React.PropTypes.string.isRequired,
+    dispatch: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  componentWillMount () {
+    if (!this.props.account) {
+      this.context.router.replace('/');
+    }
+  },
+
+  componentDidMount () {
+    if (!this.props.account) {
+      return;
+    }
+
+    this.props.dispatch(fetchAccountTimeline(this.props.account.get('id')));
+  },
+
+  componentWillReceiveProps (nextProps) {
+    if (this.props.account !== nextProps.account && nextProps.account) {
+      this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id')));
+    }
+  },
+
+  handleCommentChange (e) {
+    this.props.dispatch(changeReportComment(e.target.value));
+  },
+
+  handleSubmit () {
+    this.props.dispatch(submitReport());
+    this.context.router.replace('/');
+  },
+
+  render () {
+    const { account, comment, intl, statusIds, isSubmitting } = this.props;
+
+    if (!account) {
+      return null;
+    }
+
+    return (
+      <Column heading={intl.formatMessage(messages.heading)} icon='flag'>
+        <ColumnBackButtonSlim />
+        <div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}>
+          <div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}>
+            <FormattedMessage id='report.target' defaultMessage='Reporting' />
+            <strong>{account.get('acct')}</strong>
+          </div>
+
+          <div style={{ flex: '1 1 auto' }} className='scrollable'>
+            <div>
+              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
+            </div>
+          </div>
+
+          <div style={{ flex: '0 0 160px', padding: '10px' }}>
+            <textarea
+              className='report__textarea'
+              placeholder={intl.formatMessage(messages.placeholder)}
+              value={comment}
+              onChange={this.handleCommentChange}
+              style={textareaStyle}
+              disabled={isSubmitting}
+            />
+
+            <div style={{ marginTop: '10px', overflow: 'hidden' }}>
+              <div style={{ float: 'right' }}><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div>
+            </div>
+          </div>
+        </div>
+      </Column>
+    );
+  }
+
+});
+
+export default connect(makeMapStateToProps)(injectIntl(Report));
diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx
index 0e92acf55..2aebcd709 100644
--- a/app/assets/javascripts/components/features/status/components/action_bar.jsx
+++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx
@@ -6,10 +6,11 @@ import { defineMessages, injectIntl } from 'react-intl';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
-  mention: { id: 'status.mention', defaultMessage: 'Mention' },
+  mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
   reply: { id: 'status.reply', defaultMessage: 'Reply' },
   reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
-  favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }
+  favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
+  report: { id: 'status.report', defaultMessage: 'Report @{name}' }
 });
 
 const ActionBar = React.createClass({
@@ -25,6 +26,7 @@ const ActionBar = React.createClass({
     onFavourite: React.PropTypes.func.isRequired,
     onDelete: React.PropTypes.func.isRequired,
     onMention: React.PropTypes.func.isRequired,
+    onReport: React.PropTypes.func,
     me: React.PropTypes.number.isRequired,
     intl: React.PropTypes.object.isRequired
   },
@@ -51,6 +53,11 @@ const ActionBar = React.createClass({
     this.props.onMention(this.props.status.get('account'), this.context.router);
   },
 
+  handleReport () {
+    this.props.onReport(this.props.status);
+    this.context.router.push('/report');
+  },
+
   render () {
     const { status, me, intl } = this.props;
 
@@ -59,13 +66,15 @@ const ActionBar = React.createClass({
     if (me === status.getIn(['account', 'id'])) {
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
     } else {
-      menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
+      menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+      menu.push(null);
+      menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
     }
 
     return (
       <div className='detailed-status__action-bar'>
         <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
-        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
+        <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton disabled={status.get('visibility') === 'direct' || status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'direct' ? 'envelope' : (status.get('visibility') === 'private' ? 'lock' : 'retweet')} onClick={this.handleReblogClick} /></div>
         <div style={{ flex: '1 1 auto', textAlign: 'center' }}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
         <div style={{ flex: '1 1 auto', textAlign: 'center' }}><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" /></div>
       </div>
diff --git a/app/assets/javascripts/components/features/status/components/card.jsx b/app/assets/javascripts/components/features/status/components/card.jsx
index ccb06dfd5..d016212fd 100644
--- a/app/assets/javascripts/components/features/status/components/card.jsx
+++ b/app/assets/javascripts/components/features/status/components/card.jsx
@@ -1,18 +1,6 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-const outerStyle = {
-  display: 'flex',
-  cursor: 'pointer',
-  fontSize: '14px',
-  border: '1px solid #363c4b',
-  borderRadius: '4px',
-  color: '#616b86',
-  marginTop: '14px',
-  textDecoration: 'none',
-  overflow: 'hidden'
-};
-
 const contentStyle = {
   flex: '1 1 auto',
   padding: '8px',
@@ -20,25 +8,6 @@ const contentStyle = {
   overflow: 'hidden'
 };
 
-const titleStyle = {
-  display: 'block',
-  fontWeight: '500',
-  marginBottom: '5px',
-  color: '#d9e1e8',
-  overflow: 'hidden',
-  textOverflow: 'ellipsis',
-  whiteSpace: 'nowrap'
-};
-
-const descriptionStyle = {
-  color: '#d9e1e8'
-};
-
-const imageOuterStyle = {
-  flex: '0 0 100px',
-  background: '#373b4a'
-};
-
 const imageStyle = {
   display: 'block',
   width: '100%',
@@ -77,20 +46,20 @@ const Card = React.createClass({
 
     if (card.get('image')) {
       image = (
-        <div style={imageOuterStyle}>
+        <div className='status-card__image'>
           <img src={card.get('image')} alt={card.get('title')} style={imageStyle} />
         </div>
       );
     }
 
     return (
-      <a style={outerStyle} href={card.get('url')} className='status-card'>
+      <a href={card.get('url')} className='status-card' target='_blank' rel='noopener'>
         {image}
 
-        <div style={contentStyle}>
-          <strong style={titleStyle} title={card.get('title')}>{card.get('title')}</strong>
-          <p style={descriptionStyle}>{card.get('description').substring(0, 50)}</p>
-          <span style={hostStyle}>{getHostname(card.get('url'))}</span>
+        <div className='status-card__content' style={contentStyle}>
+          <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
+          <p className='status-card__description'>{card.get('description').substring(0, 50)}</p>
+          <span className='status-card__host' style={hostStyle}>{getHostname(card.get('url'))}</span>
         </div>
       </a>
     );
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index f2d6ae48a..caa46ff3c 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -39,7 +39,7 @@ const DetailedStatus = React.createClass({
 
     if (status.get('media_attachments').size > 0) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={317} height={178} />;
+        media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />;
       } else {
         media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />;
       }
@@ -52,7 +52,7 @@ const DetailedStatus = React.createClass({
     }
 
     return (
-      <div style={{ background: '#2f3441', padding: '14px 10px' }} className='detailed-status'>
+      <div style={{ padding: '14px 10px' }} className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
           <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
           <DisplayName account={status.get('account')} />
@@ -62,7 +62,7 @@ const DetailedStatus = React.createClass({
 
         {media}
 
-        <div style={{ marginTop: '15px', color: '#616b86', fontSize: '14px', lineHeight: '18px' }}>
+        <div className='detailed-status__meta'>
           <a className='detailed-status__datetime' style={{ color: 'inherit' }} href={status.get('url')} target='_blank' rel='noopener'><FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /></a>{applicationLink} · <Link to={`/statuses/${status.get('id')}/reblogs`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-retweet' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('reblogs_count')} /></span></Link> · <Link to={`/statuses/${status.get('id')}/favourites`} style={{ color: 'inherit', textDecoration: 'none' }}><i className='fa fa-star' /><span style={{ fontWeight: '500', fontSize: '12px', marginLeft: '6px', display: 'inline-block' }}><FormattedNumber value={status.get('favourites_count')} /></span></Link>
         </div>
       </div>
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 894fa3176..f98fe1b01 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -1,28 +1,34 @@
-import { connect }           from 'react-redux';
-import PureRenderMixin       from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes    from 'react-immutable-proptypes';
-import { fetchStatus }       from '../../actions/statuses';
-import Immutable             from 'immutable';
-import EmbeddedStatus        from '../../components/status';
-import LoadingIndicator      from '../../components/loading_indicator';
-import DetailedStatus        from './components/detailed_status';
-import ActionBar             from './components/action_bar';
-import Column                from '../ui/components/column';
-import { favourite, reblog } from '../../actions/interactions';
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { fetchStatus } from '../../actions/statuses';
+import Immutable from 'immutable';
+import EmbeddedStatus from '../../components/status';
+import MissingIndicator from '../../components/missing_indicator';
+import DetailedStatus from './components/detailed_status';
+import ActionBar from './components/action_bar';
+import Column from '../ui/components/column';
+import {
+  favourite,
+  unfavourite,
+  reblog,
+  unreblog
+} from '../../actions/interactions';
 import {
   replyCompose,
   mentionCompose
-}                            from '../../actions/compose';
-import { deleteStatus }      from '../../actions/statuses';
+} from '../../actions/compose';
+import { deleteStatus } from '../../actions/statuses';
+import { initReport } from '../../actions/reports';
 import {
   makeGetStatus,
   getStatusAncestors,
   getStatusDescendants
-}                            from '../../selectors';
-import { ScrollContainer }   from 'react-router-scroll';
-import ColumnBackButton      from '../../components/column_back_button';
-import StatusContainer       from '../../containers/status_container';
-import { openMedia }         from '../../actions/modal';
+} from '../../selectors';
+import { ScrollContainer } from 'react-router-scroll';
+import ColumnBackButton from '../../components/column_back_button';
+import StatusContainer from '../../containers/status_container';
+import { openModal } from '../../actions/modal';
 import { isMobile } from '../../is_mobile'
 
 const makeMapStateToProps = () => {
@@ -65,7 +71,11 @@ const Status = React.createClass({
   },
 
   handleFavouriteClick (status) {
-    this.props.dispatch(favourite(status));
+    if (status.get('favourited')) {
+      this.props.dispatch(unfavourite(status));
+    } else {
+      this.props.dispatch(favourite(status));
+    }
   },
 
   handleReplyClick (status) {
@@ -73,7 +83,11 @@ const Status = React.createClass({
   },
 
   handleReblogClick (status) {
-    this.props.dispatch(reblog(status));
+    if (status.get('reblogged')) {
+      this.props.dispatch(unreblog(status));
+    } else {
+      this.props.dispatch(reblog(status));
+    }
   },
 
   handleDeleteClick (status) {
@@ -85,7 +99,11 @@ const Status = React.createClass({
   },
 
   handleOpenMedia (media, index) {
-    this.props.dispatch(openMedia(media, index));
+    this.props.dispatch(openModal('MEDIA', { media, index }));
+  },
+
+  handleReport (status) {
+    this.props.dispatch(initReport(status.get('account'), status));
   },
 
   renderChildren (list) {
@@ -99,7 +117,8 @@ const Status = React.createClass({
     if (status === null) {
       return (
         <Column>
-          <LoadingIndicator />
+          <ColumnBackButton />
+          <MissingIndicator />
         </Column>
       );
     }
@@ -123,7 +142,7 @@ const Status = React.createClass({
             {ancestors}
 
             <DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} />
-            <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} />
+            <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} />
 
             {descendants}
           </div>
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/components/column_link.jsx b/app/assets/javascripts/components/features/ui/components/column_link.jsx
index 901a29f5c..2bd1e1017 100644
--- a/app/assets/javascripts/components/features/ui/components/column_link.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column_link.jsx
@@ -4,7 +4,6 @@ const outerStyle = {
   display: 'block',
   padding: '15px',
   fontSize: '16px',
-  color: '#fff',
   textDecoration: 'none'
 };
 
diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
new file mode 100644
index 000000000..35eb2cb0c
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx
@@ -0,0 +1,133 @@
+import LoadingIndicator from '../../../components/loading_indicator';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ExtendedVideoPlayer from '../../../components/extended_video_player';
+import ImageLoader from 'react-imageloader';
+import { defineMessages, injectIntl } from 'react-intl';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  close: { id: 'lightbox.close', defaultMessage: 'Close' }
+});
+
+const leftNavStyle = {
+  position: 'absolute',
+  background: 'rgba(0, 0, 0, 0.5)',
+  padding: '30px 15px',
+  cursor: 'pointer',
+  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',
+  fontSize: '24px',
+  top: '0',
+  right: '-61px',
+  boxSizing: 'border-box',
+  height: '100%',
+  display: 'flex',
+  alignItems: 'center'
+};
+
+const closeStyle = {
+  position: 'absolute',
+  top: '4px',
+  right: '4px'
+};
+
+const MediaModal = React.createClass({
+
+  propTypes: {
+    media: ImmutablePropTypes.list.isRequired,
+    index: React.PropTypes.number.isRequired,
+    onClose: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  getInitialState () {
+    return {
+      index: null
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleNextClick () {
+    this.setState({ index: (this.getIndex() + 1) % this.props.media.size});
+  },
+
+  handlePrevClick () {
+    this.setState({ index: (this.getIndex() - 1) % this.props.media.size});
+  },
+
+  handleKeyUp (e) {
+    switch(e.key) {
+    case 'ArrowLeft':
+      this.handlePrevClick();
+      break;
+    case 'ArrowRight':
+      this.handleNextClick();
+      break;
+    }
+  },
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+  },
+
+  getIndex () {
+    return this.state.index !== null ? this.state.index : this.props.index;
+  },
+
+  render () {
+    const { media, intl, onClose } = this.props;
+
+    const index = this.getIndex();
+    const attachment = media.get(index);
+    const url = attachment.get('url');
+
+    let leftNav, rightNav, content;
+
+    leftNav = rightNav = content = '';
+
+    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>;
+    }
+
+    if (attachment.get('type') === 'image') {
+      content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />;
+    } else if (attachment.get('type') === 'gifv') {
+      content = <ExtendedVideoPlayer src={url} />;
+    }
+
+    return (
+      <div className='modal-root__modal media-modal'>
+        {leftNav}
+
+        <div>
+          <IconButton title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} style={closeStyle} />
+          {content}
+        </div>
+
+        {rightNav}
+      </div>
+    );
+  }
+
+});
+
+export default injectIntl(MediaModal);
diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
new file mode 100644
index 000000000..d2ae5e145
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx
@@ -0,0 +1,80 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import MediaModal from './media_modal';
+import { TransitionMotion, spring } from 'react-motion';
+
+const MODAL_COMPONENTS = {
+  'MEDIA': MediaModal
+};
+
+const ModalRoot = React.createClass({
+
+  propTypes: {
+    type: React.PropTypes.string,
+    props: React.PropTypes.object,
+    onClose: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleKeyUp (e) {
+    if (e.key === 'Escape' && !!this.props.type) {
+      this.props.onClose();
+    }
+  },
+
+  componentDidMount () {
+    window.addEventListener('keyup', this.handleKeyUp, false);
+  },
+
+  componentWillUnmount () {
+    window.removeEventListener('keyup', this.handleKeyUp);
+  },
+
+  willEnter () {
+    return { opacity: 0, scale: 0.98 };
+  },
+
+  willLeave () {
+    return { opacity: spring(0), scale: spring(0.98) };
+  },
+
+  render () {
+    const { type, props, onClose } = this.props;
+    const items = [];
+
+    if (!!type) {
+      items.push({
+        key: type,
+        data: { type, props },
+        style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) }
+      });
+    }
+
+    return (
+      <TransitionMotion
+        styles={items}
+        willEnter={this.willEnter}
+        willLeave={this.willLeave}>
+        {interpolatedStyles =>
+          <div className='modal-root'>
+            {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
+              const SpecificComponent = MODAL_COMPONENTS[type];
+
+              return (
+                <div key={key}>
+                  <div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} />
+                  <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
+                    <SpecificComponent {...props} onClose={onClose} />
+                  </div>
+                </div>
+              );
+            })}
+          </div>
+        }
+      </TransitionMotion>
+    );
+  }
+
+});
+
+export default ModalRoot;
diff --git a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
index 225a6a5fc..6cdb29dbf 100644
--- a/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
+++ b/app/assets/javascripts/components/features/ui/components/tabs_bar.jsx
@@ -1,15 +1,23 @@
 import { Link } from 'react-router';
 import { FormattedMessage } from 'react-intl';
 
-const TabsBar = () => {
-  return (
-    <div className='tabs-bar'>
-      <Link className='tabs-bar__link' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /> <FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /> <FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /> <FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
-      <Link className='tabs-bar__link' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
-    </div>
-  );
-};
+const TabsBar = React.createClass({
+
+  render () {
+    return (
+      <div className='tabs-bar'>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></Link>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></Link>
+        <Link className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></Link>
+
+        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public/local'><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></Link>
+        <Link className='tabs-bar__link secondary' activeClassName='active' to='/timelines/public'><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></Link>
+
+        <Link className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-bars' /></Link>
+      </div>
+    );
+  }
+
+});
 
 export default TabsBar;
diff --git a/app/assets/javascripts/components/features/ui/components/upload_area.jsx b/app/assets/javascripts/components/features/ui/components/upload_area.jsx
new file mode 100644
index 000000000..70b687019
--- /dev/null
+++ b/app/assets/javascripts/components/features/ui/components/upload_area.jsx
@@ -0,0 +1,32 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { Motion, spring } from 'react-motion';
+import { FormattedMessage } from 'react-intl';
+
+const UploadArea = React.createClass({
+
+  propTypes: {
+    active: React.PropTypes.bool
+  },
+
+  mixins: [PureRenderMixin],
+
+  render () {
+    const { active } = this.props;
+
+    return (
+      <Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
+        {({ backgroundOpacity, backgroundScale }) =>
+          <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
+            <div className='upload-area__drop'>
+              <div className='upload-area__background' style={{ transform: `translateZ(0) scale(${backgroundScale})` }} />
+              <div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
+            </div>
+          </div>
+        }
+      </Motion>
+    );
+  }
+
+});
+
+export default UploadArea;
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 334e5c199..26d77818c 100644
--- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -1,170 +1,16 @@
 import { connect } from 'react-redux';
-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';
+import { closeModal } from '../../../actions/modal';
+import ModalRoot from '../components/modal_root';
 
 const mapStateToProps = state => ({
-  media: state.getIn(['modal', 'media']),
-  index: state.getIn(['modal', 'index']),
-  isVisible: state.getIn(['modal', 'open'])
+  type: state.get('modal').modalType,
+  props: state.get('modal').modalProps
 });
 
 const mapDispatchToProps = dispatch => ({
-  onCloseClicked () {
+  onClose () {
     dispatch(closeModal());
   },
-
-  onOverlayClicked () {
-    dispatch(closeModal());
-  },
-
-  onNextClicked () {
-    dispatch(increaseIndexInModal());
-  },
-
-  onPrevClicked () {
-    dispatch(decreaseIndexInModal());
-  }
-});
-
-const imageStyle = {
-  display: 'block',
-  maxWidth: '80vw',
-  maxHeight: '80vh'
-};
-
-const loadingStyle = {
-  background: '#373b4a',
-  width: '400px',
-  paddingBottom: '120px'
-};
-
-const preloader = () => (
-  <div style={loadingStyle}>
-    <LoadingIndicator />
-  </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: {
-    media: ImmutablePropTypes.list,
-    index: React.PropTypes.number.isRequired,
-    isVisible: React.PropTypes.bool,
-    onCloseClicked: 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 { 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>
-    );
-  }
-
 });
 
-export default connect(mapStateToProps, mapDispatchToProps)(Modal);
+export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);
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);
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index 900d83dba..89fb82568 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -13,6 +13,7 @@ import { debounce } from 'react-decoration';
 import { uploadCompose } from '../../actions/compose';
 import { refreshTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
+import UploadArea from './components/upload_area';
 
 const UI = React.createClass({
 
@@ -23,7 +24,8 @@ const UI = React.createClass({
 
   getInitialState () {
     return {
-      width: window.innerWidth
+      width: window.innerWidth,
+      draggingOver: false
     };
   },
 
@@ -34,29 +36,64 @@ const UI = React.createClass({
     this.setState({ width: window.innerWidth });
   },
 
+  handleDragEnter (e) {
+    e.preventDefault();
+
+    if (!this.dragTargets) {
+      this.dragTargets = [];
+    }
+
+    if (this.dragTargets.indexOf(e.target) === -1) {
+      this.dragTargets.push(e.target);
+    }
+
+    if (e.dataTransfer && e.dataTransfer.files.length > 0) {
+      this.setState({ draggingOver: true });
+    }
+  },
+
   handleDragOver (e) {
     e.preventDefault();
     e.stopPropagation();
 
-    e.dataTransfer.dropEffect = 'copy';
+    try {
+      e.dataTransfer.dropEffect = 'copy';
+    } catch (err) {
 
-    if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') {
-      //
     }
+
+    return false;
   },
 
   handleDrop (e) {
     e.preventDefault();
 
+    this.setState({ draggingOver: false });
+
     if (e.dataTransfer && e.dataTransfer.files.length === 1) {
       this.props.dispatch(uploadCompose(e.dataTransfer.files));
     }
   },
 
+  handleDragLeave (e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
+
+    if (this.dragTargets.length > 0) {
+      return;
+    }
+
+    this.setState({ draggingOver: false });
+  },
+
   componentWillMount () {
     window.addEventListener('resize', this.handleResize, { passive: true });
-    window.addEventListener('dragover', this.handleDragOver);
-    window.addEventListener('drop', this.handleDrop);
+    document.addEventListener('dragenter', this.handleDragEnter, false);
+    document.addEventListener('dragover', this.handleDragOver, false);
+    document.addEventListener('drop', this.handleDrop, false);
+    document.addEventListener('dragleave', this.handleDragLeave, false);
 
     this.props.dispatch(refreshTimeline('home'));
     this.props.dispatch(refreshNotifications());
@@ -64,17 +101,26 @@ const UI = React.createClass({
 
   componentWillUnmount () {
     window.removeEventListener('resize', this.handleResize);
-    window.removeEventListener('dragover', this.handleDragOver);
-    window.removeEventListener('drop', this.handleDrop);
+    document.removeEventListener('dragenter', this.handleDragEnter);
+    document.removeEventListener('dragover', this.handleDragOver);
+    document.removeEventListener('drop', this.handleDrop);
+    document.removeEventListener('dragleave', this.handleDragLeave);
+  },
+
+  setRef (c) {
+    this.node = c;
   },
 
   render () {
+    const { width, draggingOver } = this.state;
+    const { children } = this.props;
+
     let mountedColumns;
 
-    if (isMobile(this.state.width)) {
+    if (isMobile(width)) {
       mountedColumns = (
         <ColumnsArea>
-          {this.props.children}
+          {children}
         </ColumnsArea>
       );
     } else {
@@ -83,13 +129,13 @@ const UI = React.createClass({
           <Compose withHeader={true} />
           <HomeTimeline trackScroll={false} />
           <Notifications trackScroll={false} />
-          {this.props.children}
+          {children}
         </ColumnsArea>
       );
     }
 
     return (
-      <div className='ui'>
+      <div className='ui' ref={this.setRef}>
         <TabsBar />
 
         {mountedColumns}
@@ -97,6 +143,7 @@ const UI = React.createClass({
         <NotificationsContainer />
         <LoadingBarContainer style={{ backgroundColor: '#2b90d9', left: '0', top: '0' }} />
         <ModalContainer />
+        <UploadArea active={draggingOver} />
       </div>
     );
   }