about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample2
-rw-r--r--.eslintrc34
-rw-r--r--.rubocop.yml1
-rw-r--r--Gemfile.lock10
-rw-r--r--README.md2
-rw-r--r--app/assets/images/mastodon-getting-started.pngbin0 -> 34539 bytes
-rw-r--r--app/assets/images/screenshot.pngbin320753 -> 249637 bytes
-rw-r--r--app/assets/javascripts/components/actions/accounts.jsx153
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx14
-rw-r--r--app/assets/javascripts/components/components/autosuggest_textarea.jsx41
-rw-r--r--app/assets/javascripts/components/components/status_list.jsx6
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx3
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx4
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx7
-rw-r--r--app/assets/javascripts/components/features/compose/containers/compose_form_container.jsx1
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx61
-rw-r--r--app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx26
-rw-r--r--app/assets/javascripts/components/features/follow_requests/index.jsx66
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx20
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/notifications/components/column_settings.jsx150
-rw-r--r--app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx17
-rw-r--r--app/assets/javascripts/components/features/notifications/index.jsx14
-rw-r--r--app/assets/javascripts/components/features/public_timeline/components/column_back_button.jsx46
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx5
-rw-r--r--app/assets/javascripts/components/features/ui/components/column.jsx3
-rw-r--r--app/assets/javascripts/components/locales/en.jsx8
-rw-r--r--app/assets/javascripts/components/reducers/accounts.jsx4
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx6
-rw-r--r--app/assets/javascripts/components/reducers/notifications.jsx23
-rw-r--r--app/assets/javascripts/components/reducers/user_lists.jsx13
-rw-r--r--app/assets/stylesheets/accounts.scss64
-rw-r--r--app/assets/stylesheets/application.scss11
-rw-r--r--app/assets/stylesheets/components.scss27
-rw-r--r--app/assets/stylesheets/forms.scss2
-rw-r--r--app/controllers/api/v1/blocks_controller.rb21
-rw-r--r--app/controllers/api/v1/favourites_controller.rb21
-rw-r--r--app/controllers/api/v1/follow_requests_controller.rb29
-rw-r--r--app/controllers/api/v1/notifications_controller.rb2
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/authorize_follow_controller.rb45
-rw-r--r--app/controllers/follow_requests_controller.rb28
-rw-r--r--app/controllers/remote_follow_controller.rb47
-rw-r--r--app/controllers/settings/preferences_controller.rb11
-rw-r--r--app/helpers/api/oembed_helper.rb2
-rw-r--r--app/helpers/authorize_follow_helper.rb4
-rw-r--r--app/helpers/follow_requests_helper.rb2
-rw-r--r--app/helpers/stream_entries_helper.rb2
-rw-r--r--app/lib/feed_manager.rb22
-rw-r--r--app/lib/tag_manager.rb2
-rw-r--r--app/mailers/notification_mailer.rb9
-rw-r--r--app/models/block.rb1
-rw-r--r--app/models/favourite.rb4
-rw-r--r--app/models/follow_request.rb4
-rw-r--r--app/models/notification.rb18
-rw-r--r--app/models/remote_follow.rb13
-rw-r--r--app/models/status.rb19
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/block_service.rb4
-rw-r--r--app/services/favourite_service.rb4
-rw-r--r--app/services/follow_service.rb7
-rw-r--r--app/services/notify_service.rb5
-rw-r--r--app/services/process_interaction_service.rb15
-rw-r--r--app/services/reblog_service.rb4
-rw-r--r--app/services/unblock_service.rb5
-rw-r--r--app/views/accounts/_grid_card.html.haml2
-rw-r--r--app/views/accounts/_header.html.haml13
-rw-r--r--app/views/api/v1/blocks/index.rabl2
-rw-r--r--app/views/api/v1/favourites/index.rabl2
-rw-r--r--app/views/api/v1/follow_requests/index.rabl2
-rw-r--r--app/views/authorize_follow/_card.html.haml11
-rw-r--r--app/views/authorize_follow/error.html.haml3
-rw-r--r--app/views/authorize_follow/new.html.haml12
-rw-r--r--app/views/follow_requests/index.html.haml16
-rw-r--r--app/views/layouts/admin.html.haml3
-rw-r--r--app/views/notification_mailer/follow_request.text.erb5
-rw-r--r--app/views/oauth/authorizations/error.html.haml5
-rw-r--r--app/views/oauth/authorizations/new.html.haml39
-rw-r--r--app/views/oauth/authorizations/show.html.haml5
-rw-r--r--app/views/remote_follow/new.html.haml13
-rw-r--r--app/views/settings/preferences/show.html.haml1
-rw-r--r--app/views/settings/shared/_links.html.haml2
-rw-r--r--app/views/xrd/webfinger.json.rabl3
-rw-r--r--app/views/xrd/webfinger.xml.ruby1
-rw-r--r--app/workers/processing_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb9
-rw-r--r--app/workers/salmon_worker.rb2
-rw-r--r--config/application.rb2
-rw-r--r--config/locales/en.yml18
-rw-r--r--config/locales/simple_form.en.yml1
-rw-r--r--config/routes.rb21
-rw-r--r--db/schema.rb1
-rw-r--r--spec/controllers/api/v1/accounts_controller_spec.rb1
-rw-r--r--spec/controllers/api/v1/blocks_controller_spec.rb19
-rw-r--r--spec/controllers/api/v1/favourites_controller_spec.rb19
-rw-r--r--spec/controllers/api/v1/follow_requests_controller_spec.rb52
-rw-r--r--spec/controllers/api/v1/statuses_controller_spec.rb1
-rw-r--r--spec/controllers/api/v1/timelines_controller_spec.rb1
-rw-r--r--spec/controllers/authorize_follow_controller_spec.rb6
-rw-r--r--spec/controllers/follow_requests_controller_spec.rb16
-rw-r--r--spec/helpers/api/oembed_helper_spec.rb15
-rw-r--r--spec/helpers/authorize_follow_helper_spec.rb5
-rw-r--r--spec/helpers/follow_requests_helper_spec.rb5
103 files changed, 1322 insertions, 216 deletions
diff --git a/.env.production.sample b/.env.production.sample
index fa1ea8338..e1e503204 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -12,7 +12,7 @@ LOCAL_DOMAIN=example.com
 LOCAL_HTTPS=true
 
 # Application secrets
-# Generate each with the `rake secret` task
+# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
 PAPERCLIP_SECRET=
 SECRET_KEY_BASE=
 
diff --git a/.eslintrc b/.eslintrc
index 10bf70546..f91385cec 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,7 +15,37 @@
     "sourceType": "module",
 
     "ecmaFeatures": {
-      "jsx": true
-    },
+      "arrowFunctions": true,
+      "jsx": true,
+      "destructuring": true,
+      "modules": true,
+      "spread": true
+    }
   },
+
+  "rules": {
+    "no-cond-assign": 2,
+    "no-console": 1,
+    "no-irregular-whitespace": 2,
+    "no-unreachable": 2,
+    "valid-typeof": 2,
+    "consistent-return": 2,
+    "dot-notation": 2,
+    "eqeqeq": 2,
+    "no-fallthrough": 2,
+    "no-unused-expressions": 2,
+    "strict": 0,
+    "no-catch-shadow": 2,
+    "indent": [1, 2],
+    "brace-style": 1,
+    "comma-spacing": [1, {"before": false, "after": true}],
+    "comma-style": [1, "last"],
+    "no-mixed-spaces-and-tabs": 1,
+    "no-nested-ternary": 1,
+    "no-trailing-spaces": 1,
+    "react/wrap-multilines": 2,
+    "react/self-closing-comp": 2,
+    "react/prop-types": 2,
+    "react/no-multi-comp": 0
+  }
 }
diff --git a/.rubocop.yml b/.rubocop.yml
index b973f01cd..28c735913 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -86,3 +86,4 @@ AllCops:
   - 'config/**/*'
   - 'bin/*'
   - 'Rakefile'
+  - 'node_modules/**/*'
diff --git a/Gemfile.lock b/Gemfile.lock
index b01ac36eb..2467b76cc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -39,7 +39,8 @@ GEM
       i18n (~> 0.7)
       minitest (~> 5.1)
       tzinfo (~> 1.1)
-    addressable (2.4.0)
+    addressable (2.5.0)
+      public_suffix (~> 2.0, >= 2.0.2)
     arel (7.1.4)
     ast (2.3.0)
     autoprefixer-rails (6.5.0.2)
@@ -98,7 +99,7 @@ GEM
       warden (~> 1.2.3)
     diff-lcs (1.2.5)
     docile (1.1.5)
-    domain_name (0.5.20160826)
+    domain_name (0.5.20161129)
       unf (>= 0.0.5, < 1.0.0)
     doorkeeper (4.2.0)
       railties (>= 4.2)
@@ -121,7 +122,7 @@ GEM
       ruby-progressbar (~> 1.4)
     globalid (0.3.7)
       activesupport (>= 4.1.0)
-    goldfinger (1.1.0)
+    goldfinger (1.1.2)
       addressable (~> 2.4)
       http (~> 2.0)
       nokogiri (~> 1.6)
@@ -138,7 +139,7 @@ GEM
     highline (1.7.8)
     hiredis (0.6.1)
     htmlentities (4.3.4)
-    http (2.0.3)
+    http (2.1.0)
       addressable (~> 2.3)
       http-cookie (~> 1.0)
       http-form_data (~> 1.0.1)
@@ -226,6 +227,7 @@ GEM
       slop (~> 3.4)
     pry-rails (0.3.4)
       pry (>= 0.9.10)
+    public_suffix (2.0.4)
     puma (3.6.0)
     rabl (0.13.1)
       activesupport (>= 2.3.14)
diff --git a/README.md b/README.md
index 3add10473..2d84062a7 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ An alternative implementation of the GNU social project. Based on ActivityStream
 
 Click on the screenshot to watch a demo of the UI:
 
-[![Screenshot](https://i.imgur.com/pNieDFp.png)][youtube_demo]
+[![Screenshot](https://i.imgur.com/T2q5V65.png)][youtube_demo]
 
 [youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
 
diff --git a/app/assets/images/mastodon-getting-started.png b/app/assets/images/mastodon-getting-started.png
new file mode 100644
index 000000000..e05dd493f
--- /dev/null
+++ b/app/assets/images/mastodon-getting-started.png
Binary files differdiff --git a/app/assets/images/screenshot.png b/app/assets/images/screenshot.png
index 96446906f..f248fd514 100644
--- a/app/assets/images/screenshot.png
+++ b/app/assets/images/screenshot.png
Binary files differdiff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx
index 759435afe..8d28b051f 100644
--- a/app/assets/javascripts/components/actions/accounts.jsx
+++ b/app/assets/javascripts/components/actions/accounts.jsx
@@ -51,6 +51,22 @@ export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST';
 export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS';
 export const RELATIONSHIPS_FETCH_FAIL    = 'RELATIONSHIPS_FETCH_FAIL';
 
+export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST';
+export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS';
+export const FOLLOW_REQUESTS_FETCH_FAIL    = 'FOLLOW_REQUESTS_FETCH_FAIL';
+
+export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST';
+export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
+export const FOLLOW_REQUESTS_EXPAND_FAIL    = 'FOLLOW_REQUESTS_EXPAND_FAIL';
+
+export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
+export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
+export const FOLLOW_REQUEST_AUTHORIZE_FAIL    = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
+
+export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
+export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
+export const FOLLOW_REQUEST_REJECT_FAIL    = 'FOLLOW_REQUEST_REJECT_FAIL';
+
 export function setAccountSelf(account) {
   return {
     type: ACCOUNT_SET_SELF,
@@ -509,3 +525,140 @@ export function fetchRelationshipsFail(error) {
     error
   };
 };
+
+export function fetchFollowRequests() {
+  return (dispatch, getState) => {
+    dispatch(fetchFollowRequestsRequest());
+
+    api(getState).get('/api/v1/follow_requests').then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null))
+    }).catch(error => dispatch(fetchFollowRequestsFail(error)));
+  };
+};
+
+export function fetchFollowRequestsRequest() {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_REQUEST
+  };
+};
+
+export function fetchFollowRequestsSuccess(accounts, next) {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_SUCCESS,
+    accounts,
+    next
+  };
+};
+
+export function fetchFollowRequestsFail(error) {
+  return {
+    type: FOLLOW_REQUESTS_FETCH_FAIL,
+    error
+  };
+};
+
+export function expandFollowRequests() {
+  return (dispatch, getState) => {
+    const url = getState().getIn(['user_lists', 'follow_requests', 'next']);
+
+    if (url === null) {
+      return;
+    }
+
+    dispatch(expandFollowRequestsRequest());
+
+    api(getState).get(url).then(response => {
+      const next = getLinks(response).refs.find(link => link.rel === 'next');
+      dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null))
+    }).catch(error => dispatch(expandFollowRequestsFail(error)));
+  };
+};
+
+export function expandFollowRequestsRequest() {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_REQUEST
+  };
+};
+
+export function expandFollowRequestsSuccess(accounts, next) {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_SUCCESS,
+    accounts,
+    next
+  };
+};
+
+export function expandFollowRequestsFail(error) {
+  return {
+    type: FOLLOW_REQUESTS_EXPAND_FAIL,
+    error
+  };
+};
+
+export function authorizeFollowRequest(id) {
+  return (dispatch, getState) => {
+    dispatch(authorizeFollowRequestRequest(id));
+
+    api(getState)
+      .post(`/api/v1/follow_requests/${id}/authorize`)
+      .then(response => dispatch(authorizeFollowRequestSuccess(id)))
+      .catch(error => dispatch(authorizeFollowRequestFail(id, error)));
+  };
+};
+
+export function authorizeFollowRequestRequest(id) {
+  return {
+    type: FOLLOW_REQUEST_AUTHORIZE_REQUEST,
+    id
+  };
+};
+
+export function authorizeFollowRequestSuccess(id) {
+  return {
+    type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+    id
+  };
+};
+
+export function authorizeFollowRequestFail(id, error) {
+  return {
+    type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
+    id,
+    error
+  };
+};
+
+
+export function rejectFollowRequest(id) {
+  return (dispatch, getState) => {
+    dispatch(rejectFollowRequestRequest(id));
+
+    api(getState)
+      .post(`/api/v1/follow_requests/${id}/reject`)
+      .then(response => dispatch(rejectFollowRequestSuccess(id)))
+      .catch(error => dispatch(rejectFollowRequestFail(id, error)));
+  };
+};
+
+export function rejectFollowRequestRequest(id) {
+  return {
+    type: FOLLOW_REQUEST_REJECT_REQUEST,
+    id
+  };
+};
+
+export function rejectFollowRequestSuccess(id) {
+  return {
+    type: FOLLOW_REQUEST_REJECT_SUCCESS,
+    id
+  };
+};
+
+export function rejectFollowRequestFail(id, error) {
+  return {
+    type: FOLLOW_REQUEST_REJECT_FAIL,
+    id,
+    error
+  };
+};
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx
index 6a8b1b05b..8bd835406 100644
--- a/app/assets/javascripts/components/actions/notifications.jsx
+++ b/app/assets/javascripts/components/actions/notifications.jsx
@@ -14,6 +14,8 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
 export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
 export const NOTIFICATIONS_EXPAND_FAIL    = 'NOTIFICATIONS_EXPAND_FAIL';
 
+export const NOTIFICATIONS_SETTING_CHANGE = 'NOTIFICATIONS_SETTING_CHANGE';
+
 const fetchRelatedRelationships = (dispatch, notifications) => {
   const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
 
@@ -23,7 +25,7 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
 };
 
 export function updateNotifications(notification, intlMessages, intlLocale) {
-  return dispatch => {
+  return (dispatch, getState) => {
     dispatch({
       type: NOTIFICATIONS_UPDATE,
       notification,
@@ -34,7 +36,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
     fetchRelatedRelationships(dispatch, [notification]);
 
     // Desktop notifications
-    if (typeof window.Notification !== 'undefined') {
+    if (typeof window.Notification !== 'undefined' && getState().getIn(['notifications', 'settings', 'alerts', notification.type], false)) {
       const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
       const body  = $('<p>').html(notification.status ? notification.status.content : '').text();
 
@@ -131,3 +133,11 @@ export function expandNotificationsFail(error) {
     error
   };
 };
+
+export function changeNotificationsSetting(key, checked) {
+  return {
+    type: NOTIFICATIONS_SETTING_CHANGE,
+    key,
+    checked
+  };
+};
diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
index 8d9da1601..39ccbcaf9 100644
--- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx
+++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
@@ -32,6 +32,7 @@ const AutosuggestTextarea = React.createClass({
     value: React.PropTypes.string,
     suggestions: ImmutablePropTypes.list,
     disabled: React.PropTypes.bool,
+    fileDropDate: React.PropTypes.instanceOf(Date),
     placeholder: React.PropTypes.string,
     onSuggestionSelected: React.PropTypes.func.isRequired,
     onSuggestionsClearRequested: React.PropTypes.func.isRequired,
@@ -42,6 +43,8 @@ const AutosuggestTextarea = React.createClass({
 
   getInitialState () {
     return {
+      isFileDragging: false,
+      fileDraggingDate: undefined,
       suggestionsHidden: false,
       selectedSuggestion: 0,
       lastToken: null,
@@ -120,21 +123,51 @@ const AutosuggestTextarea = React.createClass({
     if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
       this.setState({ suggestionsHidden: false });
     }
+
+    const fileDropDate = nextProps.fileDropDate;
+    const { isFileDragging, fileDraggingDate } = this.state;
+
+    /*
+     * We can't detect drop events, because they might not be on the textarea (the app allows dropping anywhere in the
+     * window). Instead, on-drop, we notify this textarea to stop its hover effect by passing in a prop with the
+     * drop-date.
+     */
+    if (isFileDragging && fileDraggingDate && fileDropDate // if dragging when props updated, and dates aren't undefined
+      && fileDropDate > fileDraggingDate) { // and if the drop date is now greater than when we started dragging
+      // then we should stop dragging
+      this.setState({
+        isFileDragging: false
+      });
+    }
   },
 
   setTextarea (c) {
     this.textarea = c;
   },
 
+  onDragEnter () {
+    this.setState({
+      isFileDragging: true,
+      fileDraggingDate: new Date()
+    })
+  },
+
+  onDragExit () {
+    this.setState({
+      isFileDragging: false
+    })
+  },
+
   render () {
-    const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
-    const { suggestionsHidden, selectedSuggestion } = this.state;
+    const { value, suggestions, fileDropDate, disabled, placeholder, onKeyUp } = this.props;
+    const { isFileDragging, suggestionsHidden, selectedSuggestion } = this.state;
+    const className = isFileDragging ? 'autosuggest-textarea__textarea file-drop' : 'autosuggest-textarea__textarea';
 
     return (
       <div className='autosuggest-textarea'>
         <textarea
           ref={this.setTextarea}
-          className='autosuggest-textarea__textarea'
+          className={className}
           disabled={disabled}
           placeholder={placeholder}
           value={value}
@@ -142,6 +175,8 @@ const AutosuggestTextarea = React.createClass({
           onKeyDown={this.onKeyDown}
           onKeyUp={onKeyUp}
           onBlur={this.onBlur}
+          onDragEnter={this.onDragEnter}
+          onDragExit={this.onDragExit}
         />
 
         <div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
index b48d94405..e0a73435f 100644
--- a/app/assets/javascripts/components/components/status_list.jsx
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -27,11 +27,11 @@ const StatusList = React.createClass({
 
     this._oldScrollPosition = scrollHeight - scrollTop;
 
-    if (scrollTop === scrollHeight - clientHeight) {
+    if (scrollTop === scrollHeight - clientHeight && this.props.onScrollToBottom) {
       this.props.onScrollToBottom();
-    } else if (scrollTop < 100) {
+    } else if (scrollTop < 100 && this.props.onScrollToTop) {
       this.props.onScrollToTop();
-    } else {
+    } else if (this.props.onScroll) {
       this.props.onScroll();
     }
   },
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 69fe2d07f..670455376 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -34,6 +34,7 @@ import Reblogs from '../features/reblogs';
 import Favourites from '../features/favourites';
 import HashtagTimeline from '../features/hashtag_timeline';
 import Notifications from '../features/notifications';
+import FollowRequests from '../features/follow_requests';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import en from 'react-intl/locale-data/en';
 import de from 'react-intl/locale-data/de';
@@ -125,6 +126,8 @@ const Mastodon = React.createClass({
                 <Route path='followers' component={Followers} />
                 <Route path='following' component={Following} />
               </Route>
+
+              <Route path='follow_requests' component={FollowRequests} />
             </Route>
           </Router>
         </Provider>
diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx
index adf9ab5ae..6ae5ac002 100644
--- a/app/assets/javascripts/components/features/account/components/header.jsx
+++ b/app/assets/javascripts/components/features/account/components/header.jsx
@@ -61,10 +61,10 @@ const Header = React.createClass({
     const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
 
     return (
-      <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', backgroundPosition: 'center', position: 'relative' }}>
+      <div className='account__header' style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', backgroundPosition: 'center', position: 'relative' }}>
         <div style={{ background: 'rgba(47, 52, 65, 0.9)', padding: '20px 10px' }}>
           <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
-            <div style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
+            <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>
 
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 760b0efd1..55f361b0b 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -20,12 +20,14 @@ const messages = defineMessages({
 const ComposeForm = React.createClass({
 
   propTypes: {
+    intl: React.PropTypes.object.isRequired,
     text: React.PropTypes.string.isRequired,
     suggestion_token: React.PropTypes.string,
     suggestions: ImmutablePropTypes.list,
     sensitive: React.PropTypes.bool,
     unlisted: React.PropTypes.bool,
     private: React.PropTypes.bool,
+    fileDropDate: React.PropTypes.instanceOf(Date),
     is_submitting: React.PropTypes.bool,
     is_uploading: React.PropTypes.bool,
     in_reply_to: ImmutablePropTypes.map,
@@ -109,6 +111,7 @@ const ComposeForm = React.createClass({
           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}
@@ -129,7 +132,7 @@ const ComposeForm = React.createClass({
           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
         </label>
 
-        <Motion defaultStyle={{ opacity: 100, height: 39.5 }} style={{ opacity: spring(this.props.private ? 0 : 100), height: spring(this.props.private ? 0 : 39.5) }}>
+        <Motion defaultStyle={{ opacity: this.props.private ? 0 : 100, height: this.props.private ? 39.5 : 0 }} style={{ opacity: spring(this.props.private ? 0 : 100), height: spring(this.props.private ? 0 : 39.5) }}>
           {({ opacity, height }) =>
             <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
               <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
@@ -138,7 +141,7 @@ const ComposeForm = React.createClass({
           }
         </Motion>
 
-        <Motion defaultStyle={{ opacity: 100, height: 39.5 }} style={{ opacity: spring(this.props.media_count === 0 ? 0 : 100), height: spring(this.props.media_count === 0 ? 0 : 39.5) }}>
+        <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 style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
               <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
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 1d8f20ca7..2b6ee1ae7 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
@@ -24,6 +24,7 @@ const makeMapStateToProps = () => {
       sensitive: state.getIn(['compose', 'sensitive']),
       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'])),
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
new file mode 100644
index 000000000..0d41d192f
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -0,0 +1,61 @@
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Permalink from '../../../components/permalink';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import emojify from '../../../emoji';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
+  reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }
+});
+
+const outerStyle = {
+  padding: '14px 10px'
+};
+
+const panelStyle = {
+  background: '#2f3441',
+  display: 'flex',
+  flexDirection: 'row',
+  borderTop: '1px solid #363c4b',
+  borderBottom: '1px solid #363c4b',
+  padding: '10px 0'
+};
+
+const btnStyle = {
+  flex: '1 1 auto',
+  textAlign: 'center'
+};
+
+const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
+  const content = { __html: emojify(account.get('note')) };
+
+  return (
+    <div>
+      <div style={outerStyle}>
+        <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
+          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
+          <DisplayName account={account} />
+        </Permalink>
+
+        <div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
+      </div>
+
+      <div 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>
+    </div>
+  )
+};
+
+AccountAuthorize.propTypes = {
+  account: ImmutablePropTypes.map.isRequired,
+  onAuthorize: React.PropTypes.func.isRequired,
+  onReject: React.PropTypes.func.isRequired,
+  intl: React.PropTypes.object.isRequired
+};
+
+export default injectIntl(AccountAuthorize);
diff --git a/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx b/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx
new file mode 100644
index 000000000..da1e5eaa1
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/containers/account_authorize_container.jsx
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import AccountAuthorize from '../components/account_authorize';
+import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, props) => ({
+    account: getAccount(state, props.id)
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+  onAuthorize (account) {
+    dispatch(authorizeFollowRequest(id));
+  },
+
+  onReject (account) {
+    dispatch(rejectFollowRequest(id));
+  }
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize);
diff --git a/app/assets/javascripts/components/features/follow_requests/index.jsx b/app/assets/javascripts/components/features/follow_requests/index.jsx
new file mode 100644
index 000000000..461370999
--- /dev/null
+++ b/app/assets/javascripts/components/features/follow_requests/index.jsx
@@ -0,0 +1,66 @@
+import { connect } from 'react-redux';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import { ScrollContainer } from 'react-router-scroll';
+import Column from '../ui/components/column';
+import AccountAuthorizeContainer from './containers/account_authorize_container';
+import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }
+});
+
+const mapStateToProps = state => ({
+  accountIds: state.getIn(['user_lists', 'follow_requests', 'items'])
+});
+
+const FollowRequests = React.createClass({
+  propTypes: {
+    params: React.PropTypes.object.isRequired,
+    dispatch: React.PropTypes.func.isRequired,
+    accountIds: ImmutablePropTypes.list,
+    intl: React.PropTypes.object.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  componentWillMount () {
+    this.props.dispatch(fetchFollowRequests());
+  },
+
+  handleScroll (e) {
+    const { scrollTop, scrollHeight, clientHeight } = e.target;
+
+    if (scrollTop === scrollHeight - clientHeight) {
+      this.props.dispatch(expandFollowRequests());
+    }
+  },
+
+  render () {
+    const { intl, accountIds } = this.props;
+
+    if (!accountIds) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    return (
+      <Column icon='users' heading={intl.formatMessage(messages.heading)}>
+        <ScrollContainer scrollKey='follow_requests'>
+          <div className='scrollable' onScroll={this.handleScroll}>
+            {accountIds.map(id =>
+              <AccountAuthorizeContainer key={id} id={id} />
+            )}
+          </div>
+        </ScrollContainer>
+      </Column>
+    );
+  }
+});
+
+export default connect(mapStateToProps)(injectIntl(FollowRequests));
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index bff75f86f..157bdf8f2 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -3,15 +3,17 @@ import ColumnLink from '../ui/components/column_link';
 import { Link } from 'react-router';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
+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' },
-  settings: { id: 'navigation_bar.settings', defaultMessage: 'Settings' }
+  settings: { id: 'navigation_bar.settings', defaultMessage: 'Settings' },
+  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }
 });
 
 const mapStateToProps = state => ({
-  me: state.getIn(['meta', 'me'])
+  me: state.getIn(['accounts', state.getIn(['meta', 'me'])])
 });
 
 const hamburgerStyle = {
@@ -26,12 +28,19 @@ const hamburgerStyle = {
 };
 
 const GettingStarted = ({ intl, me }) => {
+  let followRequests = '';
+
+  if (me.get('locked')) {
+    followRequests = <ColumnLink icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />;
+  }
+
   return (
     <Column icon='asterisk' heading={intl.formatMessage(messages.heading)}>
       <div style={{ position: 'relative' }}>
         <div style={hamburgerStyle}><i className='fa fa-bars' /></div>
         <ColumnLink icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />
         <ColumnLink icon='cog' text={intl.formatMessage(messages.settings)} href='/settings/profile' />
+        {followRequests}
       </div>
 
       <div className='static-content'>
@@ -39,8 +48,15 @@ const GettingStarted = ({ intl, me }) => {
         <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>
       </div>
+
+      <div className='getting-started__illustration' />
     </Column>
   );
 };
 
+GettingStarted.propTypes = {
+  intl: React.PropTypes.object.isRequired,
+  me: ImmutablePropTypes.map.isRequired
+};
+
 export default connect(mapStateToProps)(injectIntl(GettingStarted));
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
index cf53a7729..f28e01a00 100644
--- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -7,6 +7,7 @@ import {
   updateTimeline,
   deleteFromTimelines
 } from '../../actions/timelines';
+import ColumnBackButton from '../public_timeline/components/column_back_button';
 
 const HashtagTimeline = React.createClass({
 
@@ -68,6 +69,7 @@ const HashtagTimeline = React.createClass({
 
     return (
       <Column icon='hashtag' heading={id}>
+        <ColumnBackButton />
         <StatusListContainer type='tag' id={id} />
       </Column>
     );
diff --git a/app/assets/javascripts/components/features/notifications/components/column_settings.jsx b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
new file mode 100644
index 000000000..b4035c20d
--- /dev/null
+++ b/app/assets/javascripts/components/features/notifications/components/column_settings.jsx
@@ -0,0 +1,150 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Toggle from 'react-toggle';
+import { Motion, spring } from 'react-motion';
+import { FormattedMessage } from 'react-intl';
+
+const outerStyle = {
+  background: '#373b4a',
+  padding: '15px'
+};
+
+const iconStyle = {
+  fontSize: '16px',
+  padding: '15px',
+  position: 'absolute',
+  right: '0',
+  top: '-48px',
+  cursor: 'pointer'
+};
+
+const labelStyle = {
+  display: 'block',
+  lineHeight: '24px',
+  verticalAlign: 'middle'
+};
+
+const labelSpanStyle = {
+  display: 'inline-block',
+  verticalAlign: 'middle',
+  marginBottom: '14px',
+  marginLeft: '8px',
+  color: '#9baec8'
+};
+
+const sectionStyle = {
+  cursor: 'default',
+  display: 'block',
+  fontWeight: '500',
+  color: '#9baec8',
+  marginBottom: '10px'
+};
+
+const rowStyle = {
+
+};
+
+const ColumnSettings = React.createClass({
+
+  propTypes: {
+    settings: ImmutablePropTypes.map.isRequired,
+    onChange: React.PropTypes.func.isRequired
+  },
+
+  getInitialState () {
+    return {
+      collapsed: true
+    };
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleToggleCollapsed () {
+    this.setState({ collapsed: !this.state.collapsed });
+  },
+
+  handleChange (key, e) {
+    this.props.onChange(key, e.target.checked);
+  },
+
+  render () {
+    const { settings }  = this.props;
+    const { collapsed } = this.state;
+
+    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
+    const showStr  = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
+
+    return (
+      <div style={{ position: 'relative' }}>
+        <div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className='fa fa-sliders' /></div>
+
+        <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : 458) }}>
+          {({ opacity, height }) =>
+            <div style={{ overflow: 'hidden', height: `${height}px`, opacity: opacity / 100 }}>
+              <div style={outerStyle}>
+                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
+
+                <div style={rowStyle}>
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['alerts', 'follow'])} onChange={this.handleChange.bind(this, ['alerts', 'follow'])} />
+                    <span style={labelSpanStyle}>{alertStr}</span>
+                  </label>
+
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['shows', 'follow'])} onChange={this.handleChange.bind(this, ['shows', 'follow'])} />
+                    <span style={labelSpanStyle}>{showStr}</span>
+                  </label>
+                </div>
+
+                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span>
+
+                <div style={rowStyle}>
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['alerts', 'favourite'])} onChange={this.handleChange.bind(this, ['alerts', 'favourite'])} />
+                    <span style={labelSpanStyle}>{alertStr}</span>
+                  </label>
+
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['shows', 'favourite'])} onChange={this.handleChange.bind(this, ['shows', 'favourite'])} />
+                    <span style={labelSpanStyle}>{showStr}</span>
+                  </label>
+                </div>
+
+                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
+
+                <div style={rowStyle}>
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['alerts', 'mention'])} onChange={this.handleChange.bind(this, ['alerts', 'mention'])} />
+                    <span style={labelSpanStyle}>{alertStr}</span>
+                  </label>
+
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['shows', 'mention'])} onChange={this.handleChange.bind(this, ['shows', 'mention'])} />
+                    <span style={labelSpanStyle}>{showStr}</span>
+                  </label>
+                </div>
+
+                <span style={sectionStyle}><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
+
+                <div style={rowStyle}>
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['alerts', 'reblog'])} onChange={this.handleChange.bind(this, ['alerts', 'reblog'])} />
+                    <span style={labelSpanStyle}>{alertStr}</span>
+                  </label>
+
+                  <label style={labelStyle}>
+                    <Toggle checked={settings.getIn(['shows', 'reblog'])} onChange={this.handleChange.bind(this, ['shows', 'reblog'])} />
+                    <span style={labelSpanStyle}>{showStr}</span>
+                  </label>
+                </div>
+              </div>
+            </div>
+          }
+        </Motion>
+      </div>
+    );
+  }
+
+});
+
+export default ColumnSettings;
diff --git a/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
new file mode 100644
index 000000000..6907fd351
--- /dev/null
+++ b/app/assets/javascripts/components/features/notifications/containers/column_settings_container.jsx
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+import ColumnSettings from '../components/column_settings';
+import { changeNotificationsSetting } from '../../../actions/notifications';
+
+const mapStateToProps = state => ({
+  settings: state.getIn(['notifications', 'settings'])
+});
+
+const mapDispatchToProps = dispatch => ({
+
+  onChange (key, checked) {
+    dispatch(changeNotificationsSetting(key, checked));
+  }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
index 218196cfd..7e706ad6a 100644
--- a/app/assets/javascripts/components/features/notifications/index.jsx
+++ b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -9,13 +9,21 @@ import {
 import NotificationContainer from './containers/notification_container';
 import { ScrollContainer } from 'react-router-scroll';
 import { defineMessages, injectIntl } from 'react-intl';
+import ColumnSettingsContainer from './containers/column_settings_container';
+import { createSelector } from 'reselect';
+import Immutable from 'immutable';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' }
 });
 
+const getNotifications = createSelector([
+  state => Immutable.List(state.getIn(['notifications', 'settings', 'shows']).filter(item => !item).keys()),
+  state => state.getIn(['notifications', 'items'])
+], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
+
 const mapStateToProps = state => ({
-  notifications: state.getIn(['notifications', 'items'])
+  notifications: getNotifications(state)
 });
 
 const Notifications = React.createClass({
@@ -23,7 +31,8 @@ const Notifications = React.createClass({
   propTypes: {
     notifications: ImmutablePropTypes.list.isRequired,
     dispatch: React.PropTypes.func.isRequired,
-    trackScroll: React.PropTypes.bool
+    trackScroll: React.PropTypes.bool,
+    intl: React.PropTypes.object.isRequired
   },
 
   getDefaultProps () {
@@ -69,6 +78,7 @@ const Notifications = React.createClass({
     } else {
       return (
         <Column icon='bell' heading={intl.formatMessage(messages.title)}>
+          <ColumnSettingsContainer />
           {scrollableArea}
         </Column>
       );
diff --git a/app/assets/javascripts/components/features/public_timeline/components/column_back_button.jsx b/app/assets/javascripts/components/features/public_timeline/components/column_back_button.jsx
new file mode 100644
index 000000000..4535f8f28
--- /dev/null
+++ b/app/assets/javascripts/components/features/public_timeline/components/column_back_button.jsx
@@ -0,0 +1,46 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import { FormattedMessage } from 'react-intl';
+
+const outerStyle = {
+  position: 'absolute',
+  right: '0',
+  top: '-48px',
+  padding: '15px',
+  fontSize: '16px',
+  background: '#2f3441',
+  flex: '0 0 auto',
+  cursor: 'pointer',
+  color: '#2b90d9'
+};
+
+const iconStyle = {
+  display: 'inline-block',
+  marginRight: '5px'
+};
+
+const ColumnBackButton = React.createClass({
+
+  contextTypes: {
+    router: React.PropTypes.object
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleClick () {
+    this.context.router.push('/');
+  },
+
+  render () {
+    return (
+      <div style={{ position: 'relative' }}>
+        <div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
+          <i className='fa fa-fw fa-chevron-left' style={iconStyle} />
+          <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
+        </div>
+      </div>
+    );
+  }
+
+});
+
+export default ColumnBackButton;
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
index c3da09a09..eac85f01b 100644
--- a/app/assets/javascripts/components/features/public_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -8,6 +8,7 @@ import {
   deleteFromTimelines
 } from '../../actions/timelines';
 import { defineMessages, injectIntl } from 'react-intl';
+import ColumnBackButton from './components/column_back_button';
 
 const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Public' }
@@ -16,7 +17,8 @@ const messages = defineMessages({
 const PublicTimeline = React.createClass({
 
   propTypes: {
-    dispatch: React.PropTypes.func.isRequired
+    dispatch: React.PropTypes.func.isRequired,
+    intl: React.PropTypes.object.isRequired
   },
 
   mixins: [PureRenderMixin],
@@ -53,6 +55,7 @@ const PublicTimeline = React.createClass({
 
     return (
       <Column icon='globe' heading={intl.formatMessage(messages.title)}>
+        <ColumnBackButton />
         <StatusListContainer type='public' />
       </Column>
     );
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
index c2060749a..c382e108d 100644
--- a/app/assets/javascripts/components/features/ui/components/column.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -40,7 +40,8 @@ const Column = React.createClass({
 
   propTypes: {
     heading: React.PropTypes.string,
-    icon: React.PropTypes.string
+    icon: React.PropTypes.string,
+    children: React.PropTypes.node
   },
 
   mixins: [PureRenderMixin],
diff --git a/app/assets/javascripts/components/locales/en.jsx b/app/assets/javascripts/components/locales/en.jsx
index 50007a7da..3d4a38919 100644
--- a/app/assets/javascripts/components/locales/en.jsx
+++ b/app/assets/javascripts/components/locales/en.jsx
@@ -52,7 +52,13 @@ const en = {
   "notification.follow": "{name} followed you",
   "notification.favourite": "{name} favourited your status",
   "notification.reblog": "{name} boosted your status",
-  "notification.mention": "{name} mentioned you"
+  "notification.mention": "{name} mentioned you",
+  "notifications.column_settings.alert": "Desktop notifications",
+  "notifications.column_settings.show": "Show in column",
+  "notifications.column_settings.follow": "New followers:",
+  "notifications.column_settings.favourite": "Favourites:",
+  "notifications.column_settings.mention": "Mentions:",
+  "notifications.column_settings.reblog": "Boosts:",
 };
 
 export default en;
diff --git a/app/assets/javascripts/components/reducers/accounts.jsx b/app/assets/javascripts/components/reducers/accounts.jsx
index 982e63073..7f2f89d0a 100644
--- a/app/assets/javascripts/components/reducers/accounts.jsx
+++ b/app/assets/javascripts/components/reducers/accounts.jsx
@@ -6,7 +6,8 @@ import {
   FOLLOWING_FETCH_SUCCESS,
   FOLLOWING_EXPAND_SUCCESS,
   ACCOUNT_TIMELINE_FETCH_SUCCESS,
-  ACCOUNT_TIMELINE_EXPAND_SUCCESS
+  ACCOUNT_TIMELINE_EXPAND_SUCCESS,
+  FOLLOW_REQUESTS_FETCH_SUCCESS
 } from '../actions/accounts';
 import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose';
 import {
@@ -78,6 +79,7 @@ export default function accounts(state = initialState, action) {
     case FAVOURITES_FETCH_SUCCESS:
     case COMPOSE_SUGGESTIONS_READY:
     case SEARCH_SUGGESTIONS_READY:
+    case FOLLOW_REQUESTS_FETCH_SUCCESS:
       return normalizeAccounts(state, action.accounts);
     case NOTIFICATIONS_REFRESH_SUCCESS:
     case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 742272e6f..16215684e 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -30,6 +30,7 @@ const initialState = Immutable.Map({
   unlisted: false,
   private: false,
   text: '',
+  fileDropDate: null,
   in_reply_to: null,
   is_submitting: false,
   is_uploading: false,
@@ -116,7 +117,10 @@ export default function compose(state = initialState, action) {
     case COMPOSE_SUBMIT_FAIL:
       return state.set('is_submitting', false);
     case COMPOSE_UPLOAD_REQUEST:
-      return state.set('is_uploading', true);
+      return state.withMutations(map => {
+        map.set('is_uploading', true);
+        map.set('fileDropDate', new Date());
+      });
     case COMPOSE_UPLOAD_SUCCESS:
       return appendMedia(state, Immutable.fromJS(action.media));
     case COMPOSE_UPLOAD_FAIL:
diff --git a/app/assets/javascripts/components/reducers/notifications.jsx b/app/assets/javascripts/components/reducers/notifications.jsx
index 617a833d2..e0d1ccf83 100644
--- a/app/assets/javascripts/components/reducers/notifications.jsx
+++ b/app/assets/javascripts/components/reducers/notifications.jsx
@@ -1,7 +1,8 @@
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
-  NOTIFICATIONS_EXPAND_SUCCESS
+  NOTIFICATIONS_EXPAND_SUCCESS,
+  NOTIFICATIONS_SETTING_CHANGE
 } from '../actions/notifications';
 import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 import Immutable from 'immutable';
@@ -9,7 +10,23 @@ import Immutable from 'immutable';
 const initialState = Immutable.Map({
   items: Immutable.List(),
   next: null,
-  loaded: false
+  loaded: false,
+
+  settings: Immutable.Map({
+    alerts: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    }),
+
+    shows: Immutable.Map({
+      follow: true,
+      favourite: true,
+      reblog: true,
+      mention: true
+    })
+  })
 });
 
 const notificationToMap = notification => Immutable.Map({
@@ -58,6 +75,8 @@ export default function notifications(state = initialState, action) {
       return appendNormalizedNotifications(state, action.notifications, action.next);
     case ACCOUNT_BLOCK_SUCCESS:
       return filterNotifications(state, action.relationship);
+    case NOTIFICATIONS_SETTING_CHANGE:
+      return state.setIn(['settings', ...action.key], action.checked);
     default:
       return state;
   }
diff --git a/app/assets/javascripts/components/reducers/user_lists.jsx b/app/assets/javascripts/components/reducers/user_lists.jsx
index 3608e4209..36093663f 100644
--- a/app/assets/javascripts/components/reducers/user_lists.jsx
+++ b/app/assets/javascripts/components/reducers/user_lists.jsx
@@ -2,7 +2,10 @@ import {
   FOLLOWERS_FETCH_SUCCESS,
   FOLLOWERS_EXPAND_SUCCESS,
   FOLLOWING_FETCH_SUCCESS,
-  FOLLOWING_EXPAND_SUCCESS
+  FOLLOWING_EXPAND_SUCCESS,
+  FOLLOW_REQUESTS_FETCH_SUCCESS,
+  FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
+  FOLLOW_REQUEST_REJECT_SUCCESS
 } from '../actions/accounts';
 import {
   REBLOGS_FETCH_SUCCESS,
@@ -14,7 +17,8 @@ const initialState = Immutable.Map({
   followers: Immutable.Map(),
   following: Immutable.Map(),
   reblogged_by: Immutable.Map(),
-  favourited_by: Immutable.Map()
+  favourited_by: Immutable.Map(),
+  follow_requests: Immutable.Map()
 });
 
 const normalizeList = (state, type, id, accounts, next) => {
@@ -44,6 +48,11 @@ export default function userLists(state = initialState, action) {
       return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
     case FAVOURITES_FETCH_SUCCESS:
       return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+    case FOLLOW_REQUESTS_FETCH_SUCCESS:
+      return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
+    case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
+    case FOLLOW_REQUEST_REJECT_SUCCESS:
+      return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
     default:
       return state;
   }
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index e1d5043db..748bb8224 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -283,8 +283,6 @@
     }
 
     .name {
-      width: 333-20-60-15px;
-      float: left;
       padding-top: 10px;
 
       a {
@@ -326,3 +324,65 @@
   padding-bottom: 25px;
   cursor: default;
 }
+
+.account-card {
+  padding: 14px 10px;
+  background: #fff;
+  border-radius: 4px;
+  text-align: left;
+  box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
+
+  .detailed-status__display-name {
+    display: block;
+    overflow: hidden;
+    margin-bottom: 15px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    & > div {
+      float: left;
+      margin-right: 10px;
+      width: 48px;
+      height: 48px;
+    }
+
+    .avatar {
+      display: block;
+      border-radius: 4px;
+    }
+
+    .display-name {
+      display: block;
+      max-width: 100%;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      cursor: default;
+
+      strong {
+        font-weight: 500;
+        color: #282c37;
+      }
+
+      span {
+        font-size: 14px;
+        color: #9baec8;
+      }
+    }
+
+    &:hover {
+      .display-name {
+        strong {
+          text-decoration: none;
+        }
+      }
+    }
+  }
+
+  .account__header__content {
+    font-size: 14px;
+    color: #282c37;
+  }
+}
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index d05ca3795..e4c550b81 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -214,11 +214,13 @@ body {
 .footer {
   text-align: center;
   margin-top: 30px;
+  font-size: 12px;
+  color: darken(#d9e1e8, 25%);
 
   .domain {
-    font-size: 12px;
-    font-weight: 400;
-    font-family: 'Roboto Mono', monospace;
+    //font-size: 12px;
+    font-weight: 500;
+    //font-family: 'Roboto Mono', monospace;
 
     a {
       color: inherit;
@@ -227,13 +229,12 @@ body {
   }
 
   .powered-by {
-    font-size: 12px;
     font-weight: 400;
-    color: darken(#d9e1e8, 25%);
 
     a {
       color: inherit;
       text-decoration: underline;
+      font-weight: 500;
 
       &:hover {
         text-decoration: none;
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 9f2d1217f..832b9e9b1 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -147,6 +147,12 @@
   }
 }
 
+@media screen and (max-height: 800px) {
+  .account__header__avatar, .account__header__content {
+    display: none;
+  }
+}
+
 .account__header__content {
   word-wrap: break-word;
   font-weight: 300;
@@ -332,6 +338,7 @@
 
 .column {
   width: 330px;
+  position: relative;
 }
 
 .drawer {
@@ -542,13 +549,19 @@
   width: 100%;
   height: 100px;
   resize: none;
-  border: none;
   color: #282c37;
-  padding: 10px;
+  padding: 7px;
   font-family: 'Roboto';
   font-size: 14px;
   margin: 0;
   resize: vertical;
+
+  border: 3px dashed transparent;
+  transition: border-color 0.3s ease;
+
+  &.file-drop {
+    border-color: #aaa;
+  }
 }
 
 .autosuggest-textarea__suggestions {
@@ -575,3 +588,13 @@
     color: #fff;
   }
 }
+
+.getting-started__illustration {
+  width: 330px;
+  height: 235px;
+  background: image-url('mastodon-getting-started.png') no-repeat 0 0;
+  position: absolute;
+  pointer-events: none;
+  bottom: 0;
+  left: 0;
+}
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index cf9b4fba6..e6d2e85a2 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -185,7 +185,7 @@ code {
   }
 }
 
-.oauth-prompt {
+.oauth-prompt, .follow-prompt {
   margin-bottom: 30px;
   text-align: center;
   color: #9baec8;
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
new file mode 100644
index 000000000..8629242ab
--- /dev/null
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::BlocksController < ApiController
+  before_action -> { doorkeeper_authorize! :follow }
+  before_action :require_user!
+
+  respond_to :json
+
+  def index
+    results   = Block.where(account: current_account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id])
+    accounts  = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
+    @accounts = results.map { |f| accounts[f.target_account_id] }
+
+    set_account_counters_maps(@accounts)
+
+    next_path = api_v1_blocks_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT
+    prev_path = api_v1_blocks_url(since_id: results.first.id) unless results.empty?
+
+    set_pagination_headers(next_path, prev_path)
+  end
+end
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
new file mode 100644
index 000000000..a71592acd
--- /dev/null
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Api::V1::FavouritesController < ApiController
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+
+  respond_to :json
+
+  def index
+    results   = Favourite.where(account: current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id])
+    @statuses = cache_collection(Status.where(id: results.map(&:status_id)), Status)
+
+    set_maps(@statuses)
+    set_counters_maps(@statuses)
+
+    next_path = api_v1_favourites_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT
+    prev_path = api_v1_favourites_url(since_id: results.first.id) unless results.empty?
+
+    set_pagination_headers(next_path, prev_path)
+  end
+end
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
new file mode 100644
index 000000000..a30e97e71
--- /dev/null
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class Api::V1::FollowRequestsController < ApiController
+  before_action -> { doorkeeper_authorize! :follow }
+  before_action :require_user!
+
+  def index
+    results   = FollowRequest.where(target_account: current_account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id])
+    accounts  = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
+    @accounts = results.map { |f| accounts[f.account_id] }
+
+    set_account_counters_maps(@accounts)
+
+    next_path = api_v1_follow_requests_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT
+    prev_path = api_v1_follow_requests_url(since_id: results.first.id) unless results.empty?
+
+    set_pagination_headers(next_path, prev_path)
+  end
+
+  def authorize
+    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).authorize!
+    render_empty
+  end
+
+  def reject
+    FollowRequest.find_by!(account_id: params[:id], target_account: current_account).reject!
+    render_empty
+  end
+end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index a24e0beb7..c8f162cb0 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -7,7 +7,7 @@ class Api::V1::NotificationsController < ApiController
   respond_to :json
 
   def index
-    @notifications = Notification.where(account: current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
+    @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(20, params[:max_id], params[:since_id])
     @notifications = cache_collection(@notifications, Notification)
     statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e2d879d58..0a6b50a29 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -49,13 +49,13 @@ class ApplicationController < ActionController::Base
 
   def not_found
     respond_to do |format|
-      format.any { head 404 }
+      format.any  { head 404 }
     end
   end
 
   def gone
     respond_to do |format|
-      format.any { head 410 }
+      format.any  { head 410 }
     end
   end
 
diff --git a/app/controllers/authorize_follow_controller.rb b/app/controllers/authorize_follow_controller.rb
new file mode 100644
index 000000000..e866b5599
--- /dev/null
+++ b/app/controllers/authorize_follow_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class AuthorizeFollowController < ApplicationController
+  layout 'public'
+
+  before_action :authenticate_user!
+
+  def new
+    uri = Addressable::URI.parse(acct_param)
+
+    if uri.path && %w(http https).include?(uri.scheme)
+      set_account_from_url
+    else
+      set_account_from_acct
+    end
+
+    render :error if @account.nil?
+  end
+
+  def create
+    @account = FollowService.new.call(current_account, acct_param).try(:target_account)
+
+    if @account.nil?
+      render :error
+    else
+      redirect_to web_url("accounts/#{@account.id}")
+    end
+  rescue ActiveRecord::RecordNotFound, Mastodon::NotPermitted
+    render :error
+  end
+
+  private
+
+  def set_account_from_url
+    @account = FetchRemoteAccountService.new.call(acct_param)
+  end
+
+  def set_account_from_acct
+    @account = FollowRemoteAccountService.new.call(acct_param)
+  end
+
+  def acct_param
+    params[:acct].gsub(/\Aacct:/, '')
+  end
+end
diff --git a/app/controllers/follow_requests_controller.rb b/app/controllers/follow_requests_controller.rb
deleted file mode 100644
index d4368f773..000000000
--- a/app/controllers/follow_requests_controller.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-class FollowRequestsController < ApplicationController
-  layout 'auth'
-
-  before_action :authenticate_user!
-  before_action :set_follow_request, except: :index
-
-  def index
-    @follow_requests = FollowRequest.where(target_account: current_account)
-  end
-
-  def authorize
-    @follow_request.authorize!
-    redirect_to follow_requests_path
-  end
-
-  def reject
-    @follow_request.reject!
-    redirect_to follow_requests_path
-  end
-
-  private
-
-  def set_follow_request
-    @follow_request = FollowRequest.find(params[:id])
-  end
-end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
new file mode 100644
index 000000000..7d4bfe6ce
--- /dev/null
+++ b/app/controllers/remote_follow_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class RemoteFollowController < ApplicationController
+  layout 'public'
+
+  before_action :set_account
+  before_action :check_account_suspension
+
+  def new
+    @remote_follow = RemoteFollow.new
+  end
+
+  def create
+    @remote_follow = RemoteFollow.new(resource_params)
+
+    if @remote_follow.valid?
+      resource          = Goldfinger.finger("acct:#{@remote_follow.acct}")
+      redirect_url_link = resource&.link('http://ostatus.org/schema/1.0/subscribe')
+
+      if redirect_url_link.nil? || redirect_url_link.template.nil?
+        @remote_follow.errors.add(:acct, I18n.t('remote_follow.missing_resource'))
+        render(:new) && return
+      end
+
+      redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
+    else
+      render :new
+    end
+  rescue Goldfinger::Error
+    @remote_follow.errors.add(:acct, I18n.t('remote_follow.missing_resource'))
+    render :new
+  end
+
+  private
+
+  def resource_params
+    params.require(:remote_follow).permit(:acct)
+  end
+
+  def set_account
+    @account = Account.find_local!(params[:account_username])
+  end
+
+  def check_account_suspension
+    head 410 if @account.suspended?
+  end
+end
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 692cf95ac..3b6d109a6 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -8,10 +8,11 @@ class Settings::PreferencesController < ApplicationController
   def show; end
 
   def update
-    current_user.settings(:notification_emails).follow    = user_params[:notification_emails][:follow]    == '1'
-    current_user.settings(:notification_emails).reblog    = user_params[:notification_emails][:reblog]    == '1'
-    current_user.settings(:notification_emails).favourite = user_params[:notification_emails][:favourite] == '1'
-    current_user.settings(:notification_emails).mention   = user_params[:notification_emails][:mention]   == '1'
+    current_user.settings(:notification_emails).follow         = user_params[:notification_emails][:follow]         == '1'
+    current_user.settings(:notification_emails).follow_request = user_params[:notification_emails][:follow_request] == '1'
+    current_user.settings(:notification_emails).reblog         = user_params[:notification_emails][:reblog]         == '1'
+    current_user.settings(:notification_emails).favourite      = user_params[:notification_emails][:favourite]      == '1'
+    current_user.settings(:notification_emails).mention        = user_params[:notification_emails][:mention]        == '1'
 
     current_user.settings(:interactions).must_be_follower  = user_params[:interactions][:must_be_follower]  == '1'
     current_user.settings(:interactions).must_be_following = user_params[:interactions][:must_be_following] == '1'
@@ -26,6 +27,6 @@ class Settings::PreferencesController < ApplicationController
   private
 
   def user_params
-    params.require(:user).permit(:locale, notification_emails: [:follow, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following])
+    params.require(:user).permit(:locale, notification_emails: [:follow, :follow_request, :reblog, :favourite, :mention], interactions: [:must_be_follower, :must_be_following])
   end
 end
diff --git a/app/helpers/api/oembed_helper.rb b/app/helpers/api/oembed_helper.rb
deleted file mode 100644
index 05d5ca216..000000000
--- a/app/helpers/api/oembed_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module Api::OembedHelper
-end
diff --git a/app/helpers/authorize_follow_helper.rb b/app/helpers/authorize_follow_helper.rb
new file mode 100644
index 000000000..99ee03c2f
--- /dev/null
+++ b/app/helpers/authorize_follow_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module AuthorizeFollowHelper
+end
diff --git a/app/helpers/follow_requests_helper.rb b/app/helpers/follow_requests_helper.rb
deleted file mode 100644
index cfd350e53..000000000
--- a/app/helpers/follow_requests_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module FollowRequestsHelper
-end
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 5cd65008e..ae2f575b5 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -10,7 +10,7 @@ module StreamEntriesHelper
   end
 
   def avatar_for_status_url(status)
-    status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original)
+    status.reblog? ? status.reblog.account.avatar.url(:original) : status.account.avatar.url(:original)
   end
 
   def entry_classes(status, is_predecessor, is_successor, include_threads)
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index c6262f211..0056321fa 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -78,15 +78,15 @@ class FeedManager
   def filter_from_home?(status, receiver)
     should_filter = false
 
-    if status.reply? && !status.thread.account.nil?                         # Filter out if it's a reply
-      should_filter   = !receiver.following?(status.thread.account)         # and I'm not following the person it's a reply to
-      should_filter &&= !(receiver.id == status.thread.account_id)          # and it's not a reply to me
-      should_filter &&= !(status.account_id == status.thread.account_id)    # and it's not a self-reply
-    elsif status.reblog?                                                    # Filter out a reblog
-      should_filter = receiver.blocking?(status.reblog.account)             # if I'm blocking the reblogged person
+    if status.reply? && !status.in_reply_to_account_id.nil?                   # Filter out if it's a reply
+      should_filter   = !receiver.following?(status.in_reply_to_account)      # and I'm not following the person it's a reply to
+      should_filter &&= !(receiver.id == status.in_reply_to_account_id)       # and it's not a reply to me
+      should_filter &&= !(status.account_id == status.in_reply_to_account_id) # and it's not a self-reply
+    elsif status.reblog?                                                      # Filter out a reblog
+      should_filter = receiver.blocking?(status.reblog.account)               # if I'm blocking the reblogged person
     end
 
-    should_filter ||= receiver.blocking?(status.mentions.map(&:account_id)) # or if it mentions someone I blocked
+    should_filter ||= receiver.blocking?(status.mentions.map(&:account_id))   # or if it mentions someone I blocked
 
     should_filter
   end
@@ -98,8 +98,8 @@ class FeedManager
     should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them
     should_filter ||= (status.private_visibility? && !receiver.following?(status.account))  # or if the mentioned account is not permitted to see the private status
 
-    if status.reply? && !status.thread.account.nil?                                         # or it's a reply
-      should_filter ||= receiver.blocking?(status.thread.account)                           # to a user I blocked
+    if status.reply? && !status.in_reply_to_account_id.nil?                                 # or it's a reply
+      should_filter ||= receiver.blocking?(status.in_reply_to_account)                      # to a user I blocked
     end
 
     should_filter
@@ -109,8 +109,8 @@ class FeedManager
     should_filter   = receiver.blocking?(status.account)
     should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account))
 
-    if status.reply? && !status.thread.account.nil?
-      should_filter ||= receiver.blocking?(status.thread.account)
+    if status.reply? && !status.in_reply_to_account_id.nil?
+      should_filter ||= receiver.blocking?(status.in_reply_to_account)
     elsif status.reblog?
       should_filter ||= receiver.blocking?(status.reblog.account)
     end
diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb
index 78dec28aa..4af433200 100644
--- a/app/lib/tag_manager.rb
+++ b/app/lib/tag_manager.rb
@@ -14,6 +14,8 @@ class TagManager
     delete:     'http://activitystrea.ms/schema/1.0/delete',
     follow:     'http://activitystrea.ms/schema/1.0/follow',
     unfollow:   'http://ostatus.org/schema/1.0/unfollow',
+    block:      'http://mastodon.social/schema/1.0/block',
+    unblock:    'http://mastodon.social/schema/1.0/unblock',
   }.freeze
 
   TYPES = {
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index 7b2cac7f3..a1b084682 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -40,4 +40,13 @@ class NotificationMailer < ApplicationMailer
       mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
     end
   end
+
+  def follow_request(recipient, notification)
+    @me      = recipient
+    @account = notification.from_account
+
+    I18n.with_locale(@me.user.locale || I18n.default_locale) do
+      mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
+    end
+  end
 end
diff --git a/app/models/block.rb b/app/models/block.rb
index ad225d180..c2067c5b8 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class Block < ApplicationRecord
+  include Paginable
   include Streamable
 
   belongs_to :account
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 2fc3d18cd..147105e48 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -29,6 +29,10 @@ class Favourite < ApplicationRecord
     thread
   end
 
+  def hidden?
+    status.private_visibility?
+  end
+
   before_validation do
     self.status = status.reblog if status.reblog?
   end
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 132316fb4..8eef3abf4 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -1,9 +1,13 @@
 # frozen_string_literal: true
 
 class FollowRequest < ApplicationRecord
+  include Paginable
+
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
+  has_one :notification, as: :activity, dependent: :destroy
+
   validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
 
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 9d076ad41..c0b5c45a8 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -8,16 +8,18 @@ class Notification < ApplicationRecord
   belongs_to :from_account, class_name: 'Account'
   belongs_to :activity, polymorphic: true
 
-  belongs_to :mention,   foreign_type: 'Mention',   foreign_key: 'activity_id'
-  belongs_to :status,    foreign_type: 'Status',    foreign_key: 'activity_id'
-  belongs_to :follow,    foreign_type: 'Follow',    foreign_key: 'activity_id'
-  belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id'
+  belongs_to :mention,        foreign_type: 'Mention',       foreign_key: 'activity_id'
+  belongs_to :status,         foreign_type: 'Status',        foreign_key: 'activity_id'
+  belongs_to :follow,         foreign_type: 'Follow',        foreign_key: 'activity_id'
+  belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id'
+  belongs_to :favourite,      foreign_type: 'Favourite',     foreign_key: 'activity_id'
 
   validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
 
   STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
 
   scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
+  scope :browserable, -> { where.not(activity_type: ['FollowRequest']) }
 
   cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
 
@@ -30,7 +32,7 @@ class Notification < ApplicationRecord
     when 'Status'
       :reblog
     else
-      activity_type.downcase.to_sym
+      activity_type.underscore.to_sym
     end
   end
 
@@ -43,6 +45,10 @@ class Notification < ApplicationRecord
     end
   end
 
+  def browserable?
+    type != :follow_request
+  end
+
   class << self
     def reload_stale_associations!(cached_items)
       account_ids = cached_items.map(&:from_account_id).uniq
@@ -61,7 +67,7 @@ class Notification < ApplicationRecord
 
   def set_from_account
     case activity_type
-    when 'Status', 'Follow', 'Favourite'
+    when 'Status', 'Follow', 'Favourite', 'FollowRequest'
       self.from_account_id = activity(false)&.account_id
     when 'Mention'
       self.from_account_id = activity(false)&.status&.account_id
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
new file mode 100644
index 000000000..13281a4fc
--- /dev/null
+++ b/app/models/remote_follow.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class RemoteFollow
+  include ActiveModel::Validations
+
+  attr_accessor :acct
+
+  validates :acct, presence: true
+
+  def initialize(attrs = {})
+    @acct = attrs[:acct]
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index dc7fc60d7..bc595c93b 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -8,6 +8,7 @@ class Status < ApplicationRecord
   enum visibility: [:public, :unlisted, :private], _suffix: :visibility
 
   belongs_to :account, inverse_of: :statuses
+  belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account'
 
   belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
   belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, touch: true
@@ -31,7 +32,6 @@ class Status < ApplicationRecord
 
   scope :remote, -> { where.not(uri: nil) }
   scope :local, -> { where(uri: nil) }
-  scope :permitted_for, ->(target_account, account) { account&.id == target_account.id || account&.following?(target_account) ? where('1=1') : where.not(visibility: :private) }
 
   cache_associated :account, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
 
@@ -72,7 +72,7 @@ class Status < ApplicationRecord
   end
 
   def permitted?(other_account = nil)
-    private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : true
+    private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : other_account.nil? || !account.blocking?(other_account)
   end
 
   def ancestors(account = nil)
@@ -145,6 +145,16 @@ class Status < ApplicationRecord
       end
     end
 
+    def permitted_for(target_account, account)
+      if account&.id == target_account.id || account&.following?(target_account)
+        where('1 = 1')
+      elsif !account.nil? && target_account.blocking?(account)
+        where('1 = 0')
+      else
+        where.not(visibility: :private)
+      end
+    end
+
     private
 
     def filter_timeline(query, account)
@@ -161,8 +171,9 @@ class Status < ApplicationRecord
 
   before_validation do
     text.strip!
-    self.reblog = reblog.reblog if reblog? && reblog.reblog?
-    self.in_reply_to_account_id = thread.account_id if reply?
+
+    self.reblog                 = reblog.reblog if reblog? && reblog.reblog?
+    self.in_reply_to_account_id = (thread.account_id == account_id && thread.reply? ? thread.in_reply_to_account_id : thread.account_id) if reply?
     self.visibility             = (account.locked? ? :private : :public) if visibility.nil?
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 3fc028a6a..d5a52da06 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,7 +15,7 @@ class User < ApplicationRecord
   scope :admins,   -> { where(admin: true) }
 
   has_settings do |s|
-    s.key :notification_emails, defaults: { follow: false, reblog: false, favourite: false, mention: false }
+    s.key :notification_emails, defaults: { follow: false, reblog: false, favourite: false, mention: false, follow_request: true }
     s.key :interactions, defaults: { must_be_follower: false, must_be_following: false }
   end
 
diff --git a/app/services/block_service.rb b/app/services/block_service.rb
index 66146a72a..b08cf8ca8 100644
--- a/app/services/block_service.rb
+++ b/app/services/block_service.rb
@@ -7,10 +7,12 @@ class BlockService < BaseService
     UnfollowService.new.call(account, target_account) if account.following?(target_account)
     UnfollowService.new.call(target_account, account) if target_account.following?(account)
 
-    account.block!(target_account)
+    block = account.block!(target_account)
 
     clear_timelines(account, target_account)
     clear_notifications(account, target_account)
+
+    NotificationWorker.perform_async(block.stream_entry.id, target_account.id) unless target_account.local?
   end
 
   private
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 5c04cfee4..d5fbd29e9 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -6,12 +6,14 @@ class FavouriteService < BaseService
   # @param [Status] status
   # @return [Favourite]
   def call(account, status)
+    raise Mastodon::NotPermitted unless status.permitted?(account)
+
     favourite = Favourite.create!(account: account, status: status)
 
     Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id)
 
     if status.local?
-      NotifyService.new.call(status.account, favourite)
+      NotifyService.new.call(favourite.status.account, favourite)
     else
       NotificationWorker.perform_async(favourite.stream_entry.id, status.account_id)
     end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index a73ec344d..555f01b6d 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -20,7 +20,12 @@ class FollowService < BaseService
   private
 
   def request_follow(source_account, target_account)
-    FollowRequest.create!(account: source_account, target_account: target_account)
+    return unless target_account.local?
+
+    follow_request = FollowRequest.create!(account: source_account, target_account: target_account)
+    NotifyService.new.call(target_account, follow_request)
+
+    follow_request
   end
 
   def direct_follow(source_account, target_account)
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 8263c4376..2fb1d3919 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -32,6 +32,10 @@ class NotifyService < BaseService
     false
   end
 
+  def blocked_follow_request?
+    false
+  end
+
   def blocked?
     blocked   = @recipient.suspended?                                                                                             # Skip if the recipient account is suspended anyway
     blocked ||= @recipient.id == @notification.from_account.id                                                                    # Skip for interactions with self
@@ -45,6 +49,7 @@ class NotifyService < BaseService
 
   def create_notification
     @notification.save!
+    return unless @notification.browserable?
     FeedManager.instance.broadcast(@recipient.id, type: 'notification', message: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification))
   end
 
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index 3d3cccb6a..11ec0d2dd 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -30,7 +30,7 @@ class ProcessInteractionService < BaseService
 
       case verb(xml)
       when :follow
-        follow!(account, target_account) unless target_account.locked?
+        follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account)
       when :unfollow
         unfollow!(account, target_account)
       when :favorite
@@ -41,6 +41,10 @@ class ProcessInteractionService < BaseService
         add_post!(body, account) unless status(xml).nil?
       when :delete
         delete_post!(xml, account)
+      when :block
+        reflect_block!(account, target_account)
+      when :unblock
+        reflect_unblock!(account, target_account)
       end
     end
   rescue Goldfinger::Error, HTTP::Error, OStatus2::BadSalmonError
@@ -74,6 +78,15 @@ class ProcessInteractionService < BaseService
     account.unfollow!(target_account)
   end
 
+  def reflect_block!(account, target_account)
+    UnfollowService.new.call(target_account, account) if target_account.following?(account)
+    account.block!(target_account)
+  end
+
+  def reflect_unblock!(account, target_account)
+    UnblockService.new.call(account, target_account)
+  end
+
   def delete_post!(xml, account)
     status = Status.find(xml.at_xpath('//xmlns:id', xmlns: TagManager::XMLNS).content)
 
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 23b35ffd2..0cb51eecd 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -14,9 +14,9 @@ class ReblogService < BaseService
     Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
 
     if reblogged_status.local?
-      NotifyService.new.call(reblogged_status.account, reblog)
+      NotifyService.new.call(reblog.reblog.account, reblog)
     else
-      NotificationWorker.perform_async(reblog.stream_entry.id, reblogged_status.account_id)
+      NotificationWorker.perform_async(reblog.stream_entry.id, reblog.reblog.account_id)
     end
 
     reblog
diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb
index 3658dcd71..f389364f9 100644
--- a/app/services/unblock_service.rb
+++ b/app/services/unblock_service.rb
@@ -2,6 +2,9 @@
 
 class UnblockService < BaseService
   def call(account, target_account)
-    account.unblock!(target_account) if account.blocking?(target_account)
+    return unless account.blocking?(target_account)
+
+    unblock = account.unblock!(target_account)
+    NotificationWorker.perform_async(unblock.stream_entry.id, target_account.id) unless target_account.local?
   end
 end
diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml
index dfd7a9f5e..dfdb23161 100644
--- a/app/views/accounts/_grid_card.html.haml
+++ b/app/views/accounts/_grid_card.html.haml
@@ -1,6 +1,6 @@
 .account-grid-card
   .account-grid-card__header
-    .avatar= image_tag account.avatar.url( :original)
+    .avatar= image_tag account.avatar.url(:original)
     .name
       = link_to TagManager.instance.url_for(account) do
         %span.display_name= display_name(account)
diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml
index 12c9b069d..1c6b5f0f6 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -5,8 +5,11 @@
         = link_to t('accounts.unfollow'), unfollow_account_path(@account), data: { method: :post }, class: 'button'
       - else
         = link_to t('accounts.follow'), follow_account_path(@account), data: { method: :post }, class: 'button'
-
-  .avatar= image_tag @account.avatar.url( :original)
+  - else
+    .controls
+      .remote-follow
+        = link_to t('accounts.remote_follow'), account_remote_follow_path(@account), class: 'button'
+  .avatar= image_tag @account.avatar.url(:original)
   %h1.name
     = display_name(@account)
     %small
@@ -20,12 +23,12 @@
       .counter{ class: active_nav_class(account_url(@account)) }
         = link_to account_url(@account) do
           %span.counter-label= t('accounts.posts')
-          %span.counter-number= @account.statuses.count
+          %span.counter-number= number_with_delimiter @account.statuses.count
       .counter{ class: active_nav_class(following_account_url(@account)) }
         = link_to following_account_url(@account) do
           %span.counter-label= t('accounts.following')
-          %span.counter-number= @account.following.count
+          %span.counter-number= number_with_delimiter @account.following.count
       .counter{ class: active_nav_class(followers_account_url(@account)) }
         = link_to followers_account_url(@account) do
           %span.counter-label= t('accounts.followers')
-          %span.counter-number= @account.followers.count
+          %span.counter-number= number_with_delimiter @account.followers.count
diff --git a/app/views/api/v1/blocks/index.rabl b/app/views/api/v1/blocks/index.rabl
new file mode 100644
index 000000000..9f3b13a53
--- /dev/null
+++ b/app/views/api/v1/blocks/index.rabl
@@ -0,0 +1,2 @@
+collection @accounts
+extends 'api/v1/accounts/show'
diff --git a/app/views/api/v1/favourites/index.rabl b/app/views/api/v1/favourites/index.rabl
new file mode 100644
index 000000000..44d29d91b
--- /dev/null
+++ b/app/views/api/v1/favourites/index.rabl
@@ -0,0 +1,2 @@
+collection @statuses
+extends 'api/v1/statuses/show'
diff --git a/app/views/api/v1/follow_requests/index.rabl b/app/views/api/v1/follow_requests/index.rabl
new file mode 100644
index 000000000..9f3b13a53
--- /dev/null
+++ b/app/views/api/v1/follow_requests/index.rabl
@@ -0,0 +1,2 @@
+collection @accounts
+extends 'api/v1/accounts/show'
diff --git a/app/views/authorize_follow/_card.html.haml b/app/views/authorize_follow/_card.html.haml
new file mode 100644
index 000000000..a9b02c746
--- /dev/null
+++ b/app/views/authorize_follow/_card.html.haml
@@ -0,0 +1,11 @@
+.account-card
+  .detailed-status__display-name
+    %div
+      = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar'
+
+    %span.display-name
+      %strong= display_name(account)
+      %span= "@#{account.acct}"
+
+  - unless account.note.blank?
+    .account__header__content= Formatter.instance.simplified_format(account)
diff --git a/app/views/authorize_follow/error.html.haml b/app/views/authorize_follow/error.html.haml
new file mode 100644
index 000000000..88d33b68d
--- /dev/null
+++ b/app/views/authorize_follow/error.html.haml
@@ -0,0 +1,3 @@
+.form-container
+  .flash-message#error_explanation
+    = t('authorize_follow.error')
diff --git a/app/views/authorize_follow/new.html.haml b/app/views/authorize_follow/new.html.haml
new file mode 100644
index 000000000..95601253e
--- /dev/null
+++ b/app/views/authorize_follow/new.html.haml
@@ -0,0 +1,12 @@
+- content_for :page_title do
+  = t('authorize_follow.title', acct: @account.acct)
+
+.form-container
+  .follow-prompt
+    %h2= t('authorize_follow.prompt_html', self: current_account.username)
+
+    = render partial: 'card', locals: { account: @account }
+
+  = form_tag authorize_follow_path, method: :post, class: 'simple_form' do
+    = hidden_field_tag :acct, @account.acct
+    = button_tag t('authorize_follow.follow'), type: :submit
diff --git a/app/views/follow_requests/index.html.haml b/app/views/follow_requests/index.html.haml
deleted file mode 100644
index 8c83488de..000000000
--- a/app/views/follow_requests/index.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-- content_for :page_title do
-  = t('follow_requests.title')
-
-- if @follow_requests.empty?
-  %p.nothing-here= t('accounts.nothing_here')
-- else
-  %table.table
-    %tbody
-      - @follow_requests.each do |follow_request|
-        %tr
-          %td= link_to follow_request.account.acct, web_path("accounts/#{follow_request.account.id}")
-          %td{ style: 'text-align: right' }
-            = table_link_to 'check-circle', t('follow_requests.authorize'), authorize_follow_request_path(follow_request), method: :post
-            = table_link_to 'times-circle', t('follow_requests.reject'), reject_follow_request_path(follow_request), method: :post
-
-.form-footer= render "settings/shared/links"
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 2fc116f45..11f76a1de 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,3 +1,6 @@
+- content_for :header_tags do
+  = javascript_include_tag 'application_public'
+  
 - content_for :content do
   .admin-wrapper
     .sidebar
diff --git a/app/views/notification_mailer/follow_request.text.erb b/app/views/notification_mailer/follow_request.text.erb
new file mode 100644
index 000000000..c0d38ec67
--- /dev/null
+++ b/app/views/notification_mailer/follow_request.text.erb
@@ -0,0 +1,5 @@
+<%= display_name(@me) %>,
+
+<%= t('notification_mailer.follow_request.body', name: @account.acct) %>
+
+<%= web_url("follow_requests") %>
diff --git a/app/views/oauth/authorizations/error.html.haml b/app/views/oauth/authorizations/error.html.haml
index ee72d9740..408ca2b86 100644
--- a/app/views/oauth/authorizations/error.html.haml
+++ b/app/views/oauth/authorizations/error.html.haml
@@ -1,2 +1,3 @@
-.flash-message#error_explanation
-  = @pre_auth.error_response.body[:error_description]
+.form-container
+  .flash-message#error_explanation
+    = @pre_auth.error_response.body[:error_description]
diff --git a/app/views/oauth/authorizations/new.html.haml b/app/views/oauth/authorizations/new.html.haml
index f058e2cce..1f951c272 100644
--- a/app/views/oauth/authorizations/new.html.haml
+++ b/app/views/oauth/authorizations/new.html.haml
@@ -1,25 +1,26 @@
 - content_for :page_title do
   = t('doorkeeper.authorizations.new.title')
 
-.oauth-prompt
-  %h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
+.form-container
+  .oauth-prompt
+    %h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
 
-  %p
-    = t('doorkeeper.authorizations.new.able_to')
-    = @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe
+    %p
+      = t('doorkeeper.authorizations.new.able_to')
+      = @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe
 
-= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
-  = hidden_field_tag :client_id, @pre_auth.client.uid
-  = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
-  = hidden_field_tag :state, @pre_auth.state
-  = hidden_field_tag :response_type, @pre_auth.response_type
-  = hidden_field_tag :scope, @pre_auth.scope
-  = button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
+  = form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
+    = hidden_field_tag :client_id, @pre_auth.client.uid
+    = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
+    = hidden_field_tag :state, @pre_auth.state
+    = hidden_field_tag :response_type, @pre_auth.response_type
+    = hidden_field_tag :scope, @pre_auth.scope
+    = button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
 
-= form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
-  = hidden_field_tag :client_id, @pre_auth.client.uid
-  = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
-  = hidden_field_tag :state, @pre_auth.state
-  = hidden_field_tag :response_type, @pre_auth.response_type
-  = hidden_field_tag :scope, @pre_auth.scope
-  = button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
+  = form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
+    = hidden_field_tag :client_id, @pre_auth.client.uid
+    = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
+    = hidden_field_tag :state, @pre_auth.state
+    = hidden_field_tag :response_type, @pre_auth.response_type
+    = hidden_field_tag :scope, @pre_auth.scope
+    = button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
diff --git a/app/views/oauth/authorizations/show.html.haml b/app/views/oauth/authorizations/show.html.haml
index 897a15cee..b56667f35 100644
--- a/app/views/oauth/authorizations/show.html.haml
+++ b/app/views/oauth/authorizations/show.html.haml
@@ -1,2 +1,3 @@
-.flash-message
-  %code= params[:code]
+.form-container
+  .flash-message
+    %code= params[:code]
diff --git a/app/views/remote_follow/new.html.haml b/app/views/remote_follow/new.html.haml
new file mode 100644
index 000000000..e88ccccce
--- /dev/null
+++ b/app/views/remote_follow/new.html.haml
@@ -0,0 +1,13 @@
+.form-container
+  .follow-prompt
+    %h2= t('remote_follow.prompt')
+
+    = render partial: 'authorize_follow/card', locals: { account: @account }
+
+  = simple_form_for @remote_follow, as: :remote_follow, url: account_remote_follow_path(@account) do |f|
+    = render 'shared/error_messages', object: @remote_follow
+
+    = f.input :acct, placeholder: t('remote_follow.acct')
+
+    .actions
+      = f.button :button, t('remote_follow.proceed'), type: :submit
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index db5b9fb48..a0860c94b 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -8,6 +8,7 @@
 
   = f.simple_fields_for :notification_emails, current_user.settings(:notification_emails) do |ff|
     = ff.input :follow, as: :boolean, wrapper: :with_label
+    = ff.input :follow_request, as: :boolean, wrapper: :with_label
     = ff.input :reblog, as: :boolean, wrapper: :with_label
     = ff.input :favourite, as: :boolean, wrapper: :with_label
     = ff.input :mention, as: :boolean, wrapper: :with_label
diff --git a/app/views/settings/shared/_links.html.haml b/app/views/settings/shared/_links.html.haml
index b6a0b1fc1..44f097950 100644
--- a/app/views/settings/shared/_links.html.haml
+++ b/app/views/settings/shared/_links.html.haml
@@ -1,8 +1,6 @@
 %ul.no-list
   - if controller_name != 'profiles'
     %li= link_to t('settings.edit_profile'), settings_profile_path
-  - if controller_name != 'follow_requests'
-    %li= link_to t('follow_requests.title'), follow_requests_path
   - if controller_name != 'preferences'
     %li= link_to t('settings.preferences'), settings_preferences_path
   - if controller_name != 'registrations'
diff --git a/app/views/xrd/webfinger.json.rabl b/app/views/xrd/webfinger.json.rabl
index 0de17ac19..e637ed9d3 100644
--- a/app/views/xrd/webfinger.json.rabl
+++ b/app/views/xrd/webfinger.json.rabl
@@ -11,6 +11,7 @@ node(:links) do
     { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account) },
     { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') },
     { rel: 'salmon', href: api_salmon_url(@account.id) },
-    { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" }
+    { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" },
+    { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
   ]
 end
diff --git a/app/views/xrd/webfinger.xml.ruby b/app/views/xrd/webfinger.xml.ruby
index ee5b5fc9d..80ac71d27 100644
--- a/app/views/xrd/webfinger.xml.ruby
+++ b/app/views/xrd/webfinger.xml.ruby
@@ -6,5 +6,6 @@ Nokogiri::XML::Builder.new do |xml|
     xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom'))
     xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
     xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}")
+    xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}")
   end
 end.to_xml
diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb
index b31cd0aaf..5df404bcc 100644
--- a/app/workers/processing_worker.rb
+++ b/app/workers/processing_worker.rb
@@ -2,7 +2,7 @@
 
 class ProcessingWorker
   include Sidekiq::Worker
-  
+
   sidekiq_options backtrace: true
 
   def perform(account_id, body)
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index e88d11be0..d5437bf6b 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -7,9 +7,12 @@ class Pubsubhubbub::DistributionWorker
 
   def perform(stream_entry_id)
     stream_entry = StreamEntry.find(stream_entry_id)
-    account      = stream_entry.account
-    renderer     = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
-    payload      = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
+
+    return if stream_entry.hidden?
+
+    account  = stream_entry.account
+    renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
+    payload  = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
 
     Subscription.where(account: account).active.select('id').find_each do |subscription|
       Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb
index 0903ca487..fc95ce47f 100644
--- a/app/workers/salmon_worker.rb
+++ b/app/workers/salmon_worker.rb
@@ -2,7 +2,7 @@
 
 class SalmonWorker
   include Sidekiq::Worker
-  
+
   sidekiq_options backtrace: true
 
   def perform(account_id, body)
diff --git a/config/application.rb b/config/application.rb
index 091f9c535..79ace8521 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -45,7 +45,7 @@ module Mastodon
     config.browserify_rails.commandline_options = '--transform [ babelify --presets [ es2015 react ] ] --extension=".jsx"'
 
     config.to_prepare do
-      Doorkeeper::AuthorizationsController.layout 'auth'
+      Doorkeeper::AuthorizationsController.layout 'public'
     end
 
     config.action_dispatch.default_headers = {
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4cf958517..e166fc717 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -14,6 +14,7 @@ en:
     people_followed_by: People whom %{name} follows
     people_who_follow: People who follow %{name}
     posts: Posts
+    remote_follow: Remote follow
     unfollow: Unfollow
   application_mailer:
     signature: Mastodon notifications from %{instance}
@@ -26,6 +27,11 @@ en:
     resend_confirmation: Resend confirmation instructions
     reset_password: Reset password
     set_new_password: Set new password
+  authorize_follow:
+    error: Unfortunately, there was an error looking up the remote account
+    follow: Follow
+    prompt_html: 'You (<strong>%{self}</strong>) have requested to follow:'
+    title: Follow %{acct}
   datetime:
     distance_in_words:
       about_x_hours: "%{count}h"
@@ -40,10 +46,6 @@ en:
       x_minutes: "%{count}m"
       x_months: "%{count}mo"
       x_seconds: "%{count}s"
-  follow_requests:
-    authorize: Authorize
-    reject: Reject
-    title: Follow requests
   generic:
     changes_saved_msg: Changes successfully saved!
     powered_by: powered by %{link}
@@ -58,6 +60,9 @@ en:
     follow:
       body: "%{name} is now following you!"
       subject: "%{name} is now following you"
+    follow_request:
+      body: "%{name} has requested to follow you"
+      subject: 'Pending follower: %{name}'
     mention:
       body: 'You were mentioned by %{name} in:'
       subject: You were mentioned by %{name}
@@ -67,6 +72,11 @@ en:
   pagination:
     next: Next
     prev: Prev
+  remote_follow:
+    acct: Enter your username@domain you want to follow from
+    missing_resource: Could not find the required redirect URL for your account
+    proceed: Proceed to follow
+    prompt: 'You are going to follow:'
   settings:
     edit_profile: Edit profile
     preferences: Preferences
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 12b717877..578208700 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -25,6 +25,7 @@ en:
       notification_emails:
         favourite: Send e-mail when someone favourites your status
         follow: Send e-mail when someone follows you
+        follow_request: Send e-mail when someone requests to follow you
         mention: Send e-mail when someone mentions you
         reblog: Send e-mail when someone reblogs your status
     'no': 'No'
diff --git a/config/routes.rb b/config/routes.rb
index e8c8f619d..18c239c48 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,6 +31,9 @@ Rails.application.routes.draw do
       end
     end
 
+    get :remote_follow,  to: 'remote_follow#new'
+    post :remote_follow, to: 'remote_follow#create'
+
     member do
       get :followers
       get :following
@@ -48,12 +51,9 @@ Rails.application.routes.draw do
   resources :media, only: [:show]
   resources :tags,  only: [:show]
 
-  resources :follow_requests do
-    member do
-      post :authorize
-      post :reject
-    end
-  end
+  # Remote follow
+  get  :authorize_follow, to: 'authorize_follow#new'
+  post :authorize_follow, to: 'authorize_follow#create'
 
   namespace :admin do
     resources :pubsubhubbub, only: [:index]
@@ -103,8 +103,17 @@ Rails.application.routes.draw do
       resources :follows,  only: [:create]
       resources :media,    only: [:create]
       resources :apps,     only: [:create]
+      resources :blocks,   only: [:index]
+
+      resources :follow_requests, only: [:index] do
+        member do
+          post :authorize
+          post :reject
+        end
+      end
 
       resources :notifications, only: [:index]
+      resources :favourites,    only: [:index]
 
       resources :accounts, only: [:show] do
         collection do
diff --git a/db/schema.rb b/db/schema.rb
index 180d3b14d..b9236d42f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 20161222204147) do
     t.boolean  "sensitive",              default: false
     t.integer  "visibility",             default: 0,     null: false
     t.integer  "in_reply_to_account_id"
+    t.string   "conversation_uri"
     t.index ["account_id"], name: "index_statuses_on_account_id", using: :btree
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", using: :btree
     t.index ["reblog_of_id"], name: "index_statuses_on_reblog_of_id", using: :btree
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index e4532305b..98b284f7a 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
   let(:token) { double acceptable?: true, resource_owner_id: user.id }
 
   before do
-    stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb
new file mode 100644
index 000000000..ca20a2d17
--- /dev/null
+++ b/spec/controllers/api/v1/blocks_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::BlocksController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb
new file mode 100644
index 000000000..a6e9963e5
--- /dev/null
+++ b/spec/controllers/api/v1/favourites_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::FavouritesController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+  before do
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    it 'returns http success' do
+      get :index
+      expect(response).to have_http_status(:success)
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/follow_requests_controller_spec.rb b/spec/controllers/api/v1/follow_requests_controller_spec.rb
new file mode 100644
index 000000000..a90d2d290
--- /dev/null
+++ b/spec/controllers/api/v1/follow_requests_controller_spec.rb
@@ -0,0 +1,52 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::FollowRequestsController, type: :controller do
+  render_views
+
+  let(:user)     { Fabricate(:user, account: Fabricate(:account, username: 'alice', locked: true)) }
+  let(:token)    { double acceptable?: true, resource_owner_id: user.id }
+  let(:follower) { Fabricate(:account, username: 'bob') }
+
+  before do
+    FollowService.new.call(follower, user.account.acct)
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #index' do
+    before do
+      get :index
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+  end
+
+  describe 'POST #authorize' do
+    before do
+      post :authorize, params: { id: follower.id }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'allows follower to follow' do
+      expect(follower.following?(user.account)).to be true
+    end
+  end
+
+  describe 'POST #reject' do
+    before do
+      post :reject, params: { id: follower.id }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'removes follow request' do
+      expect(FollowRequest.where(target_account: user.account, account: follower).count).to eq 0
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index ab918fe50..d9c73f952 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
   let(:token) { double acceptable?: true, resource_owner_id: user.id }
 
   before do
-    stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
diff --git a/spec/controllers/api/v1/timelines_controller_spec.rb b/spec/controllers/api/v1/timelines_controller_spec.rb
index c94519ac5..5e9954baf 100644
--- a/spec/controllers/api/v1/timelines_controller_spec.rb
+++ b/spec/controllers/api/v1/timelines_controller_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Api::V1::TimelinesController, type: :controller do
   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
 
   before do
-    stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
     allow(controller).to receive(:doorkeeper_token) { token }
   end
 
diff --git a/spec/controllers/authorize_follow_controller_spec.rb b/spec/controllers/authorize_follow_controller_spec.rb
new file mode 100644
index 000000000..954efd53e
--- /dev/null
+++ b/spec/controllers/authorize_follow_controller_spec.rb
@@ -0,0 +1,6 @@
+require 'rails_helper'
+
+RSpec.describe AuthorizeFollowController, type: :controller do
+  describe 'GET #new'
+  describe 'POST #create'
+end
diff --git a/spec/controllers/follow_requests_controller_spec.rb b/spec/controllers/follow_requests_controller_spec.rb
deleted file mode 100644
index 72f5fd9b9..000000000
--- a/spec/controllers/follow_requests_controller_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FollowRequestsController, type: :controller do
-  render_views
-
-  before do
-    sign_in Fabricate(:user), scope: :user
-  end
-
-  describe 'GET #index' do
-    it 'returns http success' do
-      get :index
-      expect(response).to have_http_status(:success)
-    end
-  end
-end
diff --git a/spec/helpers/api/oembed_helper_spec.rb b/spec/helpers/api/oembed_helper_spec.rb
deleted file mode 100644
index 4f64cb84f..000000000
--- a/spec/helpers/api/oembed_helper_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'rails_helper'
-
-# Specs in this file have access to a helper object that includes
-# the Api::OembedHelper. For example:
-#
-# describe Api::OembedHelper do
-#   describe "string concat" do
-#     it "concats two strings with spaces" do
-#       expect(helper.concat_strings("this","that")).to eq("this that")
-#     end
-#   end
-# end
-RSpec.describe Api::OembedHelper, type: :helper do
-  pending "add some examples to (or delete) #{__FILE__}"
-end
diff --git a/spec/helpers/authorize_follow_helper_spec.rb b/spec/helpers/authorize_follow_helper_spec.rb
new file mode 100644
index 000000000..ba5b0a70b
--- /dev/null
+++ b/spec/helpers/authorize_follow_helper_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AuthorizeFollowHelper, type: :helper do
+
+end
diff --git a/spec/helpers/follow_requests_helper_spec.rb b/spec/helpers/follow_requests_helper_spec.rb
deleted file mode 100644
index e031cf402..000000000
--- a/spec/helpers/follow_requests_helper_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe FollowRequestsHelper, type: :helper do
-
-end