about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx10
-rw-r--r--app/assets/javascripts/components/actions/timelines.jsx46
-rw-r--r--app/assets/javascripts/components/components/status_list.jsx14
-rw-r--r--app/assets/javascripts/components/components/video_player.jsx9
-rw-r--r--app/assets/javascripts/components/features/hashtag_timeline/index.jsx4
-rw-r--r--app/assets/javascripts/components/features/ui/containers/status_list_container.jsx13
-rw-r--r--app/assets/javascripts/components/reducers/timelines.jsx69
-rw-r--r--app/assets/stylesheets/tables.scss32
-rw-r--r--app/controllers/accounts_controller.rb4
-rw-r--r--app/controllers/admin/accounts_controller.rb27
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/tags_controller.rb5
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/atom_builder_helper.rb6
-rw-r--r--app/helpers/stream_entries_helper.rb2
-rw-r--r--app/lib/email_validator.rb18
-rw-r--r--app/lib/feed_manager.rb9
-rw-r--r--app/models/account.rb4
-rw-r--r--app/models/concerns/cacheable.rb1
-rw-r--r--app/models/feed.rb4
-rw-r--r--app/models/media_attachment.rb2
-rw-r--r--app/models/notification.rb43
-rw-r--r--app/models/status.rb20
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/notify_service.rb1
-rw-r--r--app/views/accounts/_grid_card.html.haml2
-rw-r--r--app/views/accounts/_header.html.haml4
-rw-r--r--app/views/accounts/show.atom.ruby2
-rw-r--r--app/views/accounts/show.html.haml4
-rw-r--r--app/views/admin/accounts/index.html.haml36
-rw-r--r--app/views/admin/accounts/show.html.haml34
-rw-r--r--app/views/api/v1/accounts/show.rabl4
-rw-r--r--app/views/api/v1/media/create.rabl4
-rw-r--r--app/views/api/v1/statuses/_media.rabl4
-rw-r--r--app/views/stream_entries/_status.html.haml2
-rw-r--r--app/views/stream_entries/show.html.haml2
-rw-r--r--app/views/tags/show.html.haml4
-rw-r--r--config/initializers/blacklists.rb5
-rw-r--r--config/initializers/ostatus.rb4
-rw-r--r--config/initializers/paperclip.rb18
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20161203164520_add_from_account_id_to_notifications.rb14
-rw-r--r--db/schema.rb7
-rw-r--r--public/avatars/medium/missing.pngbin1057 -> 0 bytes
-rw-r--r--public/avatars/original/missing.png (renamed from public/avatars/large/missing.png)bin2977 -> 2977 bytes
-rw-r--r--public/avatars/small/missing.pngbin681 -> 0 bytes
-rw-r--r--spec/controllers/admin/accounts_controller_spec.rb4
-rw-r--r--spec/helpers/atom_builder_helper_spec.rb2
49 files changed, 408 insertions, 104 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index c2a7909f6..23bcf8eac 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -69,8 +69,14 @@ export function submitCompose() {
       sensitive: getState().getIn(['compose', 'sensitive']),
       unlisted: getState().getIn(['compose', 'unlisted'])
     }).then(function (response) {
-      dispatch(submitComposeSuccess(response.data));
-      dispatch(updateTimeline('home', response.data));
+      dispatch(submitComposeSuccess({ ...response.data }));
+
+      // To make the app more responsive, immediately get the status into the columns
+      dispatch(updateTimeline('home', { ...response.data }));
+
+      if (response.data.in_reply_to_id === null) {
+        dispatch(updateTimeline('public', { ...response.data }));
+      }
     }).catch(function (error) {
       dispatch(submitComposeFail(error));
     });
diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx
index 0f23ca7fc..5aab993c1 100644
--- a/app/assets/javascripts/components/actions/timelines.jsx
+++ b/app/assets/javascripts/components/actions/timelines.jsx
@@ -12,12 +12,13 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
 export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
 export const TIMELINE_EXPAND_FAIL    = 'TIMELINE_EXPAND_FAIL';
 
-export function refreshTimelineSuccess(timeline, statuses, replace) {
+export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
+
+export function refreshTimelineSuccess(timeline, statuses) {
   return {
     type: TIMELINE_REFRESH_SUCCESS,
     timeline: timeline,
-    statuses: statuses,
-    replace: replace
+    statuses: statuses
   };
 };
 
@@ -48,24 +49,25 @@ export function deleteFromTimelines(id) {
   };
 };
 
-export function refreshTimelineRequest(timeline) {
+export function refreshTimelineRequest(timeline, id) {
   return {
     type: TIMELINE_REFRESH_REQUEST,
-    timeline: timeline
+    timeline,
+    id
   };
 };
 
-export function refreshTimeline(timeline, replace = false, id = null) {
+export function refreshTimeline(timeline, id = null) {
   return function (dispatch, getState) {
-    dispatch(refreshTimelineRequest(timeline));
+    dispatch(refreshTimelineRequest(timeline, id));
 
-    const ids      = getState().getIn(['timelines', timeline], Immutable.List());
+    const ids      = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
     const newestId = ids.size > 0 ? ids.first() : null;
 
     let params = '';
     let path   = timeline;
 
-    if (newestId !== null && !replace) {
+    if (newestId !== null) {
       params = `?since_id=${newestId}`;
     }
 
@@ -74,7 +76,7 @@ export function refreshTimeline(timeline, replace = false, id = null) {
     }
 
     api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
-      dispatch(refreshTimelineSuccess(timeline, response.data, replace));
+      dispatch(refreshTimelineSuccess(timeline, response.data));
     }).catch(function (error) {
       dispatch(refreshTimelineFail(timeline, error));
     });
@@ -84,14 +86,14 @@ export function refreshTimeline(timeline, replace = false, id = null) {
 export function refreshTimelineFail(timeline, error) {
   return {
     type: TIMELINE_REFRESH_FAIL,
-    timeline: timeline,
-    error: error
+    timeline,
+    error
   };
 };
 
 export function expandTimeline(timeline, id = null) {
   return (dispatch, getState) => {
-    const lastId = getState().getIn(['timelines', timeline], Immutable.List()).last();
+    const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
 
     dispatch(expandTimelineRequest(timeline));
 
@@ -112,22 +114,30 @@ export function expandTimeline(timeline, id = null) {
 export function expandTimelineRequest(timeline) {
   return {
     type: TIMELINE_EXPAND_REQUEST,
-    timeline: timeline
+    timeline
   };
 };
 
 export function expandTimelineSuccess(timeline, statuses) {
   return {
     type: TIMELINE_EXPAND_SUCCESS,
-    timeline: timeline,
-    statuses: statuses
+    timeline,
+    statuses
   };
 };
 
 export function expandTimelineFail(timeline, error) {
   return {
     type: TIMELINE_EXPAND_FAIL,
-    timeline: timeline,
-    error: error
+    timeline,
+    error
+  };
+};
+
+export function scrollTopTimeline(timeline, top) {
+  return {
+    type: TIMELINE_SCROLL_TOP,
+    timeline,
+    top
   };
 };
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
index f989ef895..b48d94405 100644
--- a/app/assets/javascripts/components/components/status_list.jsx
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -1,14 +1,16 @@
-import Status              from './status';
-import ImmutablePropTypes  from 'react-immutable-proptypes';
-import PureRenderMixin     from 'react-addons-pure-render-mixin';
+import Status from './status';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
 import { ScrollContainer } from 'react-router-scroll';
-import StatusContainer     from '../containers/status_container';
+import StatusContainer from '../containers/status_container';
 
 const StatusList = React.createClass({
 
   propTypes: {
     statusIds: ImmutablePropTypes.list.isRequired,
     onScrollToBottom: React.PropTypes.func,
+    onScrollToTop: React.PropTypes.func,
+    onScroll: React.PropTypes.func,
     trackScroll: React.PropTypes.bool
   },
 
@@ -27,6 +29,10 @@ const StatusList = React.createClass({
 
     if (scrollTop === scrollHeight - clientHeight) {
       this.props.onScrollToBottom();
+    } else if (scrollTop < 100) {
+      this.props.onScrollToTop();
+    } else {
+      this.props.onScroll();
     }
   },
 
diff --git a/app/assets/javascripts/components/components/video_player.jsx b/app/assets/javascripts/components/components/video_player.jsx
index 61c1995a7..8f64ad3cd 100644
--- a/app/assets/javascripts/components/components/video_player.jsx
+++ b/app/assets/javascripts/components/components/video_player.jsx
@@ -53,7 +53,8 @@ const VideoPlayer = React.createClass({
   propTypes: {
     media: ImmutablePropTypes.map.isRequired,
     width: React.PropTypes.number,
-    height: React.PropTypes.number
+    height: React.PropTypes.number,
+    sensitive: React.PropTypes.bool
   },
 
   getDefaultProps () {
@@ -102,6 +103,12 @@ const VideoPlayer = React.createClass({
           <span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
         </div>
       );
+    } else if (!sensitive && !this.state.visible) {
+      return (
+        <div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
+          <div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
+        </div>
+      );
     }
 
     return (
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
index bea0a2759..cf53a7729 100644
--- a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -47,13 +47,13 @@ const HashtagTimeline = React.createClass({
     const { dispatch } = this.props;
     const { id } = this.props.params;
 
-    dispatch(refreshTimeline('tag', true, id));
+    dispatch(refreshTimeline('tag', id));
     this._subscribe(dispatch, id);
   },
 
   componentWillReceiveProps (nextProps) {
     if (nextProps.params.id !== this.props.params.id) {
-      this.props.dispatch(refreshTimeline('tag', true, nextProps.params.id));
+      this.props.dispatch(refreshTimeline('tag', nextProps.params.id));
       this._unsubscribe();
       this._subscribe(this.props.dispatch, nextProps.params.id);
     }
diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
index 8004e3f04..1621cec7b 100644
--- a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
+++ b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
@@ -1,16 +1,25 @@
 import { connect } from 'react-redux';
 import StatusList from '../../../components/status_list';
-import { expandTimeline } from '../../../actions/timelines';
+import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines';
 import Immutable from 'immutable';
 
 const mapStateToProps = (state, props) => ({
-  statusIds: state.getIn(['timelines', props.type], Immutable.List())
+  statusIds: state.getIn(['timelines', props.type, 'items'], Immutable.List())
 });
 
 const mapDispatchToProps = function (dispatch, props) {
   return {
     onScrollToBottom () {
+      dispatch(scrollTopTimeline(props.type, false));
       dispatch(expandTimeline(props.type, props.id));
+    },
+
+    onScrollToTop () {
+      dispatch(scrollTopTimeline(props.type, true));
+    },
+
+    onScroll () {
+      dispatch(scrollTopTimeline(props.type, false));
     }
   };
 };
diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx
index 358734eaf..de157eb25 100644
--- a/app/assets/javascripts/components/reducers/timelines.jsx
+++ b/app/assets/javascripts/components/reducers/timelines.jsx
@@ -1,8 +1,10 @@
 import {
+  TIMELINE_REFRESH_REQUEST,
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_DELETE,
-  TIMELINE_EXPAND_SUCCESS
+  TIMELINE_EXPAND_SUCCESS,
+  TIMELINE_SCROLL_TOP
 } from '../actions/timelines';
 import {
   REBLOG_SUCCESS,
@@ -23,10 +25,31 @@ import {
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
-  home: Immutable.List(),
-  mentions: Immutable.List(),
-  public: Immutable.List(),
-  tag: Immutable.List(),
+  home: Immutable.Map({
+    loaded: false,
+    top: true,
+    items: Immutable.List()
+  }),
+
+  mentions: Immutable.Map({
+    loaded: false,
+    top: true,
+    items: Immutable.List()
+  }),
+
+  public: Immutable.Map({
+    loaded: false,
+    top: true,
+    items: Immutable.List()
+  }),
+
+  tag: Immutable.Map({
+    id: null,
+    loaded: false,
+    top: true,
+    items: Immutable.List()
+  }),
+
   accounts_timelines: Immutable.Map(),
   ancestors: Immutable.Map(),
   descendants: Immutable.Map()
@@ -50,14 +73,17 @@ const normalizeStatus = (state, status) => {
 };
 
 const normalizeTimeline = (state, timeline, statuses, replace = false) => {
-  let ids = Immutable.List();
+  let ids      = Immutable.List();
+  const loaded = state.getIn([timeline, 'loaded']);
 
   statuses.forEach((status, i) => {
     state = normalizeStatus(state, status);
     ids   = ids.set(i, status.get('id'));
   });
 
-  return state.update(timeline, Immutable.List(), list => (replace ? ids : list.unshift(...ids)));
+  state = state.setIn([timeline, 'loaded'], true);
+
+  return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : list.push(...ids)));
 };
 
 const appendNormalizedTimeline = (state, timeline, statuses) => {
@@ -68,7 +94,7 @@ const appendNormalizedTimeline = (state, timeline, statuses) => {
     moreIds = moreIds.set(i, status.get('id'));
   });
 
-  return state.update(timeline, Immutable.List(), list => list.push(...moreIds));
+  return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
 };
 
 const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => {
@@ -94,9 +120,15 @@ const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
 };
 
 const updateTimeline = (state, timeline, status, references) => {
+  const top = state.getIn([timeline, 'top']);
+
   state = normalizeStatus(state, status);
 
-  state = state.update(timeline, Immutable.List(), list => {
+  state = state.updateIn([timeline, 'items'], Immutable.List(), list => {
+    if (top && list.size > 40) {
+      list = list.take(20);
+    }
+
     if (list.includes(status.get('id'))) {
       return list;
     }
@@ -116,7 +148,7 @@ const updateTimeline = (state, timeline, status, references) => {
 const deleteStatus = (state, id, accountId, references) => {
   // Remove references from timelines
   ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
-    state = state.update(timeline, list => list.filterNot(item => item === id));
+    state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
   });
 
   // Remove references from account timelines
@@ -166,10 +198,23 @@ const normalizeContext = (state, id, ancestors, descendants) => {
   });
 };
 
+const resetTimeline = (state, timeline, id) => {
+  if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
+    state = state.update(timeline, map => map
+        .set('id', id)
+        .set('loaded', false)
+        .update('items', list => list.clear()));
+  }
+
+  return state;
+};
+
 export default function timelines(state = initialState, action) {
   switch(action.type) {
+    case TIMELINE_REFRESH_REQUEST:
+      return resetTimeline(state, action.timeline, action.id);
     case TIMELINE_REFRESH_SUCCESS:
-      return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.replace);
+      return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
     case TIMELINE_EXPAND_SUCCESS:
       return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
     case TIMELINE_UPDATE:
@@ -184,6 +229,8 @@ export default function timelines(state = initialState, action) {
       return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
     case ACCOUNT_BLOCK_SUCCESS:
       return filterTimelines(state, action.relationship, action.statuses);
+    case TIMELINE_SCROLL_TOP:
+      return state.setIn([action.timeline, 'top'], action.top);
     default:
       return state;
   }
diff --git a/app/assets/stylesheets/tables.scss b/app/assets/stylesheets/tables.scss
index 89b35891d..b28b91191 100644
--- a/app/assets/stylesheets/tables.scss
+++ b/app/assets/stylesheets/tables.scss
@@ -3,6 +3,7 @@
   max-width: 100%;
   border-spacing: 0;
   border-collapse: collapse;
+  margin-bottom: 20px;
 
   th, td {
     padding: 8px;
@@ -18,8 +19,39 @@
     border-top: 0;
     font-weight: 500;
   }
+
+  & > tbody > tr > th {
+    font-weight: 500;
+  }
+
+  a {
+    color: #2b90d9;
+    text-decoration: underline;
+
+    &:hover {
+      text-decoration: none;
+    }
+  }
 }
 
 samp {
   font-family: 'Roboto Mono', monospace;
 }
+
+.filters {
+  list-style: none;
+  margin-bottom: 20px;
+
+  li {
+    display: inline-block;
+  }
+
+  a {
+    color: #2b90d9;
+    text-decoration: underline;
+
+    &:hover {
+      text-decoration: none;
+    }
+  }
+}
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index b0e5a8320..46231dd97 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -9,12 +9,12 @@ class AccountsController < ApplicationController
   def show
     respond_to do |format|
       format.html do
-        @statuses = @account.statuses.order('id desc').paginate_by_max_id(20, params[:max_id || nil])
+        @statuses = @account.statuses.order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id])
         @statuses = cache_collection(@statuses, Status)
       end
 
       format.atom do
-        @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id] || nil)
+        @entries = @account.stream_entries.order('id desc').with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
       end
     end
   end
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e84799040..79fb37eb9 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -2,12 +2,37 @@
 
 class Admin::AccountsController < ApplicationController
   before_action :require_admin!
+  before_action :set_account, except: :index
 
   layout 'public'
 
   def index
+    @accounts = Account.order('domain ASC, username ASC').paginate(page: params[:page], per_page: 40)
+
+    @accounts = @accounts.local                             if params[:local].present?
+    @accounts = @accounts.remote                            if params[:remote].present?
+    @accounts = @accounts.where(domain: params[:by_domain]) if params[:by_domain].present?
+    @accounts = @accounts.where(silenced: true)             if params[:silenced].present?
+    @accounts = @accounts.reorder('id desc')                if params[:recent].present?
+  end
+
+  def show; end
+
+  def update
+    if @account.update(account_params)
+      redirect_to admin_accounts_path
+    else
+      render :show
+    end
+  end
+
+  private
+
+  def set_account
+    @account = Account.find(params[:id])
   end
 
-  def show
+  def account_params
+    params.require(:account).permit(:silenced)
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index fbe4af07c..7270686de 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -61,7 +61,7 @@ class ApplicationController < ActionController::Base
   def cache_collection(raw, klass)
     return raw unless klass.respond_to?(:with_includes)
 
-    raw                    = raw.select(:id, :updated_at).to_a if raw.is_a?(ActiveRecord::Relation)
+    raw                    = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
     uncached_ids           = []
     cached_keys_with_value = Rails.cache.read_multi(*raw.map(&:cache_key))
 
@@ -69,6 +69,8 @@ class ApplicationController < ActionController::Base
       uncached_ids << item.id unless cached_keys_with_value.key?(item.cache_key)
     end
 
+    klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
+
     unless uncached_ids.empty?
       uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
 
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 4a70b2a8f..6b6c73080 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -4,7 +4,8 @@ class TagsController < ApplicationController
   layout 'public'
 
   def show
-    @statuses = Tag.find_by!(name: params[:id].downcase).statuses.order('id desc').paginate_by_max_id(20, params[:max_id] || nil)
-  	@statuses = cache_collection(@statuses, Status)
+    @tag      = Tag.find_by!(name: params[:id].downcase)
+    @statuses = @tag.statuses.order('id desc').paginate_by_max_id(20, params[:max_id])
+    @statuses = cache_collection(@statuses, Status)
   end
 end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 29c2c9120..be82ff2fe 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -4,8 +4,4 @@ module ApplicationHelper
   def active_nav_class(path)
     current_page?(path) ? 'active' : ''
   end
-
-  def id_paginate(path, per_page, collection)
-  	# todo
-  end
 end
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index 40bbe0491..953ccd438 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -112,13 +112,11 @@ module AtomBuilderHelper
   end
 
   def link_enclosure(xml, media)
-    xml.link(rel: 'enclosure', href: full_asset_url(media.file.url), type: media.file_content_type, length: media.file_file_size)
+    xml.link(rel: 'enclosure', href: full_asset_url(media.file.url(:original, false)), type: media.file_content_type, length: media.file_file_size)
   end
 
   def link_avatar(xml, account)
-    single_link_avatar(xml, account, :large, 300)
-    # single_link_avatar(xml, account, :medium, 96)
-    # single_link_avatar(xml, account, :small,  48)
+    single_link_avatar(xml, account, :original, 120)
   end
 
   def logo(xml, url)
diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb
index 1eb2ed058..0aa7008be 100644
--- a/app/helpers/stream_entries_helper.rb
+++ b/app/helpers/stream_entries_helper.rb
@@ -6,7 +6,7 @@ module StreamEntriesHelper
   end
 
   def avatar_for_status_url(status)
-    status.reblog? ? status.reblog.account.avatar.url(:large) : status.account.avatar.url(:large)
+    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/email_validator.rb b/app/lib/email_validator.rb
new file mode 100644
index 000000000..856b8b1f7
--- /dev/null
+++ b/app/lib/email_validator.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class EmailValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    return if Rails.configuration.x.email_domains_blacklist.empty?
+
+    record.errors.add(attribute, I18n.t('users.invalid_email')) if blocked_email?(value)
+  end
+
+  private
+
+  def blocked_email?(value)
+    domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.')
+    regexp  = Regexp.new("@(.+\\.)?(#{domains})", true)
+
+    value =~ regexp
+  end
+end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index b812ad1f4..e08f9a0da 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -82,12 +82,13 @@ class FeedManager
   end
 
   def filter_from_mentions?(status, receiver)
-    should_filter   = receiver.id == status.account_id                      # Filter if I'm mentioning myself
-    should_filter ||= receiver.blocking?(status.account)                    # or it's from someone I blocked
+    should_filter   = receiver.id == status.account_id                                      # Filter if I'm mentioning myself
+    should_filter ||= receiver.blocking?(status.account)                                    # or it's from someone I blocked
     should_filter ||= receiver.blocking?(status.mentions.includes(:account).map(&:account)) # or if it mentions someone I blocked
+    should_filter ||= (status.account.silenced? && !receiver.following?(status.account))    # of if the account is silenced and I'm not following them
 
-    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.thread.account.nil?                                         # or it's a reply
+      should_filter ||= receiver.blocking?(status.thread.account)                           # to a user I blocked
     end
 
     should_filter
diff --git a/app/models/account.rb b/app/models/account.rb
index 105b77e04..b1cf34e9a 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -13,12 +13,12 @@ class Account < ApplicationRecord
   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
 
   # Avatar upload
-  has_attached_file :avatar, styles: { large: '300x300#' }, convert_options: { all: '-strip' }
+  has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :avatar, less_than: 2.megabytes
 
   # Header upload
-  has_attached_file :header, styles: { medium: '700x335#' }, convert_options: { all: '-strip' }
+  has_attached_file :header, styles: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :header, less_than: 2.megabytes
 
diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb
index cd0167048..51451d260 100644
--- a/app/models/concerns/cacheable.rb
+++ b/app/models/concerns/cacheable.rb
@@ -11,5 +11,6 @@ module Cacheable
 
   included do
     scope :with_includes, -> { includes(@cache_associated) }
+    scope :cache_ids, -> { select(:id, :updated_at) }
   end
 end
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 7b181d529..5e1905e15 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -14,9 +14,9 @@ class Feed
     # If we're after most recent items and none are there, we need to precompute the feed
     if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'
       RegenerationWorker.perform_async(@account.id, @type)
-      @statuses = Status.send("as_#{@type}_timeline", @account).paginate_by_max_id(limit, nil, nil)
+      @statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil)
     else
-      status_map = Status.where(id: unhydrated).map { |s| [s.id, s] }.to_h
+      status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h
       @statuses  = unhydrated.map { |id| status_map[id] }.compact
     end
 
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index f1b9b8112..d37ef99a8 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -10,7 +10,7 @@ class MediaAttachment < ApplicationRecord
   has_attached_file :file,
                     styles: -> (f) { file_styles f },
                     processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] },
-                    convert_options: { all: '-strip' }
+                    convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES
   validates_attachment_size :file, less_than: 4.megabytes
 
diff --git a/app/models/notification.rb b/app/models/notification.rb
index b066cd87a..9d076ad41 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -5,6 +5,7 @@ class Notification < ApplicationRecord
   include Cacheable
 
   belongs_to :account
+  belongs_to :from_account, class_name: 'Account'
   belongs_to :activity, polymorphic: true
 
   belongs_to :mention,   foreign_type: 'Mention',   foreign_key: 'activity_id'
@@ -16,10 +17,12 @@ class Notification < ApplicationRecord
 
   STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :media_attachments, :tags, mentions: :account]].freeze
 
-  cache_associated status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
+  scope :cache_ids, -> { select(:id, :updated_at, :activity_type, :activity_id) }
 
-  def activity
-    send(activity_type.downcase)
+  cache_associated :from_account, status: STATUS_INCLUDES, mention: [status: STATUS_INCLUDES], favourite: [:account, status: STATUS_INCLUDES], follow: :account
+
+  def activity(eager_loaded = true)
+    eager_loaded ? send(activity_type.downcase) : super
   end
 
   def type
@@ -31,15 +34,6 @@ class Notification < ApplicationRecord
     end
   end
 
-  def from_account
-    case type
-    when :mention
-      activity.status.account
-    when :follow, :favourite, :reblog
-      activity.account
-    end
-  end
-
   def target_status
     case type
     when :reblog
@@ -48,4 +42,29 @@ class Notification < ApplicationRecord
       activity.status
     end
   end
+
+  class << self
+    def reload_stale_associations!(cached_items)
+      account_ids = cached_items.map(&:from_account_id).uniq
+      accounts    = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
+
+      cached_items.each do |item|
+        item.from_account = accounts[item.from_account_id]
+      end
+    end
+  end
+
+  after_initialize :set_from_account
+  before_validation :set_from_account
+
+  private
+
+  def set_from_account
+    case activity_type
+    when 'Status', 'Follow', 'Favourite'
+      self.from_account_id = activity(false)&.account_id
+    when 'Mention'
+      self.from_account_id = activity(false)&.status&.account_id
+    end
+  end
 end
diff --git a/app/models/status.rb b/app/models/status.rb
index 1e70101a3..1f5cf9b46 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -94,11 +94,11 @@ class Status < ApplicationRecord
 
   class << self
     def as_home_timeline(account)
-      where(account: [account] + account.following).with_includes
+      where(account: [account] + account.following)
     end
 
     def as_mentions_timeline(account)
-      where(id: Mention.where(account: account).pluck(:status_id)).with_includes
+      where(id: Mention.where(account: account).select(:status_id))
     end
 
     def as_public_timeline(account = nil)
@@ -130,6 +130,22 @@ class Status < ApplicationRecord
       select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
     end
 
+    def reload_stale_associations!(cached_items)
+      account_ids = []
+
+      cached_items.each do |item|
+        account_ids << item.account_id
+        account_ids << item.reblog.account_id if item.reblog?
+      end
+
+      accounts = Account.where(id: account_ids.uniq).map { |a| [a.id, a] }.to_h
+
+      cached_items.each do |item|
+        item.account = accounts[item.account_id]
+        item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
+      end
+    end
+
     private
 
     def filter_timeline(query, account)
diff --git a/app/models/user.rb b/app/models/user.rb
index 423833d47..3fc028a6a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -8,6 +8,7 @@ class User < ApplicationRecord
 
   validates :account, presence: true
   validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?'
+  validates :email, email: true
 
   scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') }
   scope :recent,   -> { order('id desc') }
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 1efd326b0..ab76e2a6b 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -36,6 +36,7 @@ class NotifyService < BaseService
     blocked   = false
     blocked ||= @recipient.id == @notification.from_account.id
     blocked ||= @recipient.blocking?(@notification.from_account)
+    blocked ||= (@notification.from_account.silenced? && !@recipient.following?(@notification.from_account))
     blocked ||= (@recipient.user.settings(:interactions).must_be_follower  && !@notification.from_account.following?(@recipient))
     blocked ||= (@recipient.user.settings(:interactions).must_be_following && !@recipient.following?(@notification.from_account))
     blocked ||= send("blocked_#{@notification.type}?")
diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml
index f65b78470..dfd7a9f5e 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(:medium)
+    .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 0063d9f16..c132a6896 100644
--- a/app/views/accounts/_header.html.haml
+++ b/app/views/accounts/_header.html.haml
@@ -1,4 +1,4 @@
-.card{ style: "background-image: url(#{@account.header.url(:medium)})" }
+.card{ style: "background-image: url(#{@account.header.url( :original)})" }
   - if user_signed_in? && current_account.id != @account.id
     .controls
       - if current_account.following?(@account)
@@ -6,7 +6,7 @@
       - else
         = link_to t('accounts.follow'), follow_account_path(@account), data: { method: :post }, class: 'button'
 
-  .avatar= image_tag @account.avatar.url(:large)
+  .avatar= image_tag @account.avatar.url( :original)
   %h1.name
     = display_name(@account)
     %small= "@#{@account.username}"
diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby
index 558c777f0..b2903d189 100644
--- a/app/views/accounts/show.atom.ruby
+++ b/app/views/accounts/show.atom.ruby
@@ -6,7 +6,7 @@ Nokogiri::XML::Builder.new do |xml|
     title      xml, @account.display_name
     subtitle   xml, @account.note
     updated_at xml, stream_updated_at
-    logo       xml, full_asset_url(@account.avatar.url(:medium, false))
+    logo       xml, full_asset_url(@account.avatar.url( :original))
 
     author(xml) do
       include_author xml, @account
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index c04faa32f..db8e45e6b 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -14,4 +14,6 @@
   .activity-stream
     = render partial: 'stream_entries/status', collection: @statuses, as: :status
 
-= id_paginate account_url(@account), 20, @statuses
+.pagination
+  - if @statuses.size == 20
+    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index e69de29bb..a074f0ad9 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -0,0 +1,36 @@
+%ul.filters
+  %li= link_to 'Local', admin_accounts_path(local: '1')
+  %li= link_to 'Remote', admin_accounts_path(remote: '1')
+  %li= link_to 'Silenced', admin_accounts_path(silenced: '1')
+  %li= link_to 'Most recent', admin_accounts_path(recent: '1')
+
+%table.table
+  %thead
+    %tr
+      %th Username
+      %th Domain
+      %th Subscribed
+      %th Silenced
+      %th
+  %tbody
+    - @accounts.each do |account|
+      %tr
+        %td= account.username
+        %td
+          - unless account.local?
+            = link_to account.domain, admin_accounts_path(by_domain: account.domain)
+        %td
+          - if account.local?
+            Local
+          - elsif account.subscribed?
+            %i.fa.fa-check
+          - else
+            %i.fa.fa-times
+        %td
+          - if account.silenced?
+            %i.fa.fa-check
+          - else
+            %i.fa.fa-times
+        %td= link_to 'Edit', admin_account_path(account.id)
+
+= will_paginate @accounts, pagination_options
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index e69de29bb..02f7dcfe9 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -0,0 +1,34 @@
+%table.table
+  %tbody
+    %tr
+      %th Username
+      %td= @account.username
+    %tr
+      %th Domain
+      %td= @account.domain
+    %tr
+      %th Display name
+      %td= @account.display_name
+
+    - if @account.local?
+      %tr
+        %th E-mail
+        %td= @account.user.email
+      %tr
+        %th Current IP
+        %td= @account.user.current_sign_in_ip
+    - else
+      %tr
+        %th Profile URL
+        %td= link_to @account.url
+      %tr
+        %th Feed URL
+        %td= link_to @account.remote_url
+
+= simple_form_for @account, url: admin_account_path(@account.id) do |f|
+  = render 'shared/error_messages', object: @account
+
+  = f.input :silenced, as: :boolean, wrapper: :with_label
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl
index c01349ef2..22cb87f6c 100644
--- a/app/views/api/v1/accounts/show.rabl
+++ b/app/views/api/v1/accounts/show.rabl
@@ -4,8 +4,8 @@ attributes :id, :username, :acct, :display_name
 
 node(:note)            { |account| Formatter.instance.simplified_format(account) }
 node(:url)             { |account| TagManager.instance.url_for(account) }
-node(:avatar)          { |account| full_asset_url(account.avatar.url(:large, false)) }
-node(:header)          { |account| full_asset_url(account.header.url(:medium, false)) }
+node(:avatar)          { |account| full_asset_url(account.avatar.url( :original)) }
+node(:header)          { |account| full_asset_url(account.header.url( :original)) }
 node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) }
 node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) }
 node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : (account.try(:statuses_count)  || account.statuses.count) }
diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl
index 803a93094..0b42e6e3d 100644
--- a/app/views/api/v1/media/create.rabl
+++ b/app/views/api/v1/media/create.rabl
@@ -1,5 +1,5 @@
 object @media
 attribute :id, :type
-node(:url) { |media| full_asset_url(media.file.url) }
-node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
+node(:url) { |media| full_asset_url(media.file.url( :original)) }
+node(:preview_url) { |media| full_asset_url(media.file.url( :small)) }
 node(:text_url) { |media| medium_url(media) }
diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl
index e4ceef763..af635dfec 100644
--- a/app/views/api/v1/statuses/_media.rabl
+++ b/app/views/api/v1/statuses/_media.rabl
@@ -1,4 +1,4 @@
 attributes :id, :remote_url, :type
 
-node(:url)         { |media| full_asset_url(media.file.url) }
-node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
+node(:url)         { |media| full_asset_url(media.file.url( :original)) }
+node(:preview_url) { |media| full_asset_url(media.file.url( :small)) }
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index 2edc8bc3f..8169b8178 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -34,7 +34,7 @@
       - if (status.reblog? ? status.reblog : status).media_attachments.size > 0
         %ul.media-attachments
           - (status.reblog? ? status.reblog : status).media_attachments.each do |media|
-            %li.transparent-background= link_to '', media.file.url, style: "background-image: url(#{media.file.url(:small)})", target: '_blank'
+            %li.transparent-background= link_to '', media.file.url( :original), style: "background-image: url(#{media.file.url( :small)})", target: '_blank'
 
 - if include_threads
   = render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true }
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index a0e248873..1bacaf32b 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -7,7 +7,7 @@
   %meta{ name: 'og:title', content: "#{@account.username} on #{Rails.configuration.x.local_domain}" }/
   %meta{ name: 'og:article:author', content: @account.username }/
   %meta{ name: 'og:description', content: @stream_entry.activity.content }/
-  %meta{ name: 'og:image', content: @stream_entry.activity.is_a?(Status) && @stream_entry.activity.media_attachments.size > 0 ? full_asset_url(@stream_entry.activity.media_attachments.first.file.url(:small)) : full_asset_url(@account.avatar.url(:large)) }/
+  %meta{ name: 'og:image', content: @stream_entry.activity.is_a?(Status) && @stream_entry.activity.media_attachments.size > 0 ? full_asset_url(@stream_entry.activity.media_attachments.first.file.url( :small)) : full_asset_url(@account.avatar.url( :original)) }/
 
 .activity-stream.activity-stream-headless
   = render partial: @type, locals: { @type.to_sym => @stream_entry.activity, include_threads: true }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index bfe5c0439..dd42fe22c 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -5,4 +5,6 @@
   .activity-stream
     = render partial: 'stream_entries/status', collection: @statuses, as: :status, cached: true
 
-= id_paginate tag_path, 20, @statuses
+.pagination
+  - if @statuses.size == 20
+    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
diff --git a/config/initializers/blacklists.rb b/config/initializers/blacklists.rb
new file mode 100644
index 000000000..52646e64d
--- /dev/null
+++ b/config/initializers/blacklists.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+  config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
+end
diff --git a/config/initializers/ostatus.rb b/config/initializers/ostatus.rb
index 4ba432b6a..c5723b2e9 100644
--- a/config/initializers/ostatus.rb
+++ b/config/initializers/ostatus.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
 port  = ENV.fetch('PORT') { 3000 }
 host  = ENV.fetch('LOCAL_DOMAIN') { "localhost:#{port}" }
 https = ENV['LOCAL_HTTPS'] == 'true'
-  
+
 Rails.application.configure do
   config.x.local_domain = host
   config.x.hub_url      = ENV.fetch('HUB_URL') { 'https://pubsubhubbub.superfeedr.com' }
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 80effc05e..93822a2d1 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -1,11 +1,19 @@
+# frozen_string_literal: true
+
 if ENV['S3_ENABLED'] == 'true'
   Aws.eager_autoload!(services: %w(S3))
 
-  Paperclip::Attachment.default_options[:storage]      = :s3
-  Paperclip::Attachment.default_options[:s3_protocol]  = 'https'
-  Paperclip::Attachment.default_options[:url]          = ':s3_domain_url'
-  Paperclip::Attachment.default_options[:s3_host_name] = "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com"
-  Paperclip::Attachment.default_options[:path]         = '/:class/:attachment/:id_partition/:style/:filename'
+  Paperclip::Attachment.default_options[:storage]        = :s3
+  Paperclip::Attachment.default_options[:s3_protocol]    = 'https'
+  Paperclip::Attachment.default_options[:url]            = ':s3_domain_url'
+  Paperclip::Attachment.default_options[:s3_host_name]   = "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com"
+  Paperclip::Attachment.default_options[:path]           = '/:class/:attachment/:id_partition/:style/:filename'
+  Paperclip::Attachment.default_options[:s3_headers]     = { 'Cache-Control' => 'max-age=315576000', 'Expires' => 10.years.from_now.httpdate }
+
+  unless ENV['S3_CLOUDFRONT_HOST'].blank?
+    Paperclip::Attachment.default_options[:url]           = ':s3_alias_url'
+    Paperclip::Attachment.default_options[:s3_host_alias] = ENV['S3_CLOUDFRONT_HOST']
+  end
 
   Paperclip::Attachment.default_options[:s3_credentials] = {
     bucket: ENV.fetch('S3_BUCKET'),
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 426f3928a..50a1f0e95 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -55,5 +55,7 @@ en:
   stream_entries:
     favourited: favourited a post by
     is_now_following: is now following
+  users:
+    invalid_email: The e-mail address is invalid
   will_paginate:
     page_gap: "&hellip;"
diff --git a/config/routes.rb b/config/routes.rb
index 35e5c269a..ac53bbed6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -46,7 +46,7 @@ Rails.application.routes.draw do
 
   namespace :admin do
     resources :pubsubhubbub, only: [:index]
-    resources :accounts, only: [:index, :show]
+    resources :accounts, only: [:index, :show, :update]
   end
 
   namespace :api do
diff --git a/db/migrate/20161203164520_add_from_account_id_to_notifications.rb b/db/migrate/20161203164520_add_from_account_id_to_notifications.rb
new file mode 100644
index 000000000..142adbe9c
--- /dev/null
+++ b/db/migrate/20161203164520_add_from_account_id_to_notifications.rb
@@ -0,0 +1,14 @@
+class AddFromAccountIdToNotifications < ActiveRecord::Migration[5.0]
+  def up
+    add_column :notifications, :from_account_id, :integer
+
+    Notification.where(from_account_id: nil).where(activity_type: 'Status').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN statuses ON notifications1.activity_id = statuses.id WHERE notifications1.activity_type = \'Status\' AND notifications1.id = notifications.id)')
+    Notification.where(from_account_id: nil).where(activity_type: 'Mention').update_all('from_account_id = (SELECT statuses.account_id FROM notifications AS notifications1 INNER JOIN mentions ON notifications1.activity_id = mentions.id INNER JOIN statuses ON mentions.status_id = statuses.id WHERE notifications1.activity_type = \'Mention\' AND notifications1.id = notifications.id)')
+    Notification.where(from_account_id: nil).where(activity_type: 'Favourite').update_all('from_account_id = (SELECT favourites.account_id FROM notifications AS notifications1 INNER JOIN favourites ON notifications1.activity_id = favourites.id WHERE notifications1.activity_type = \'Favourite\' AND notifications1.id = notifications.id)')
+    Notification.where(from_account_id: nil).where(activity_type: 'Follow').update_all('from_account_id = (SELECT follows.account_id FROM notifications AS notifications1 INNER JOIN follows ON notifications1.activity_id = follows.id WHERE notifications1.activity_type = \'Follow\' AND notifications1.id = notifications.id)')
+  end
+
+  def down
+    remove_column :notifications, :from_account_id
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f9080ae2d..8e5c806d4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20161202132159) do
+ActiveRecord::Schema.define(version: 20161203164520) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -100,8 +100,9 @@ ActiveRecord::Schema.define(version: 20161202132159) do
     t.integer  "account_id"
     t.integer  "activity_id"
     t.string   "activity_type"
-    t.datetime "created_at",    null: false
-    t.datetime "updated_at",    null: false
+    t.datetime "created_at",      null: false
+    t.datetime "updated_at",      null: false
+    t.integer  "from_account_id"
     t.index ["account_id", "activity_id", "activity_type"], name: "account_activity", unique: true, using: :btree
   end
 
diff --git a/public/avatars/medium/missing.png b/public/avatars/medium/missing.png
deleted file mode 100644
index 98fffdae3..000000000
--- a/public/avatars/medium/missing.png
+++ /dev/null
Binary files differdiff --git a/public/avatars/large/missing.png b/public/avatars/original/missing.png
index 53cffc312..53cffc312 100644
--- a/public/avatars/large/missing.png
+++ b/public/avatars/original/missing.png
Binary files differdiff --git a/public/avatars/small/missing.png b/public/avatars/small/missing.png
deleted file mode 100644
index 43fe8b741..000000000
--- a/public/avatars/small/missing.png
+++ /dev/null
Binary files differdiff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb
index 485310495..47b1267e8 100644
--- a/spec/controllers/admin/accounts_controller_spec.rb
+++ b/spec/controllers/admin/accounts_controller_spec.rb
@@ -13,8 +13,10 @@ RSpec.describe Admin::AccountsController, type: :controller do
   end
 
   describe 'GET #show' do
+    let(:account) { Fabricate(:account, username: 'bob') }
+
     it 'returns http success' do
-      get :show, params: { id: 1 }
+      get :show, params: { id: account.id }
       expect(response).to have_http_status(:success)
     end
   end
diff --git a/spec/helpers/atom_builder_helper_spec.rb b/spec/helpers/atom_builder_helper_spec.rb
index 8a161cab3..3d3bd56a1 100644
--- a/spec/helpers/atom_builder_helper_spec.rb
+++ b/spec/helpers/atom_builder_helper_spec.rb
@@ -162,7 +162,7 @@ RSpec.describe AtomBuilderHelper, type: :helper do
     let(:account) { Fabricate(:account, username: 'alice') }
 
     it 'creates a link' do
-      expect(used_with_namespaces { |xml| helper.link_avatar(xml, account) }).to match '<link rel="avatar" type="" media:width="300" media:height="300" href="http://test.host/avatars/large/missing.png"/>'
+      expect(used_with_namespaces { |xml| helper.link_avatar(xml, account) }).to match '<link rel="avatar" type="" media:width="120" media:height="120" href="http://test.host/avatars/original/missing.png"/>'
     end
   end