about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/components/actions/notifications.jsx12
-rw-r--r--app/assets/javascripts/components/components/account.jsx2
-rw-r--r--app/assets/javascripts/components/components/avatar.jsx135
-rw-r--r--app/assets/javascripts/components/components/status.jsx2
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx13
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx6
-rw-r--r--app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx22
-rw-r--r--app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/navigation_bar.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/reply_indicator.jsx2
-rw-r--r--app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx2
-rw-r--r--app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx12
-rw-r--r--app/assets/javascripts/components/features/notifications/components/notification.jsx8
-rw-r--r--app/assets/javascripts/components/features/status/components/detailed_status.jsx2
-rw-r--r--app/assets/javascripts/components/locales/fr.jsx2
-rw-r--r--app/assets/javascripts/components/locales/index.jsx6
-rw-r--r--app/assets/javascripts/components/locales/pt.jsx71
-rw-r--r--app/assets/javascripts/components/locales/ru.jsx68
-rw-r--r--app/assets/stylesheets/accounts.scss15
-rw-r--r--app/assets/stylesheets/components.scss146
-rw-r--r--app/controllers/about_controller.rb29
-rw-r--r--app/controllers/accounts_controller.rb6
-rw-r--r--app/controllers/admin/accounts_controller.rb93
-rw-r--r--app/controllers/admin/base_controller.rb9
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb42
-rw-r--r--app/controllers/admin/pubsubhubbub_controller.rb12
-rw-r--r--app/controllers/admin/reports_controller.rb81
-rw-r--r--app/controllers/admin/settings_controller.rb46
-rw-r--r--app/controllers/api/v1/accounts_controller.rb15
-rw-r--r--app/controllers/api/v1/notifications_controller.rb10
-rw-r--r--app/controllers/api_controller.rb1
-rw-r--r--app/controllers/concerns/localized.rb4
-rw-r--r--app/controllers/remote_follow_controller.rb2
-rw-r--r--app/controllers/settings/exports_controller.rb2
-rw-r--r--app/controllers/xrd_controller.rb2
-rw-r--r--app/helpers/about_helper.rb4
-rw-r--r--app/helpers/accounts_helper.rb12
-rw-r--r--app/helpers/admin/domain_blocks_helper.rb4
-rw-r--r--app/helpers/admin/pubsubhubbub_helper.rb4
-rw-r--r--app/helpers/atom_builder_helper.rb2
-rw-r--r--app/helpers/authorize_follow_helper.rb4
-rw-r--r--app/helpers/settings_helper.rb4
-rw-r--r--app/helpers/tags_helper.rb4
-rw-r--r--app/helpers/xrd_helper.rb4
-rw-r--r--app/lib/atom_serializer.rb4
-rw-r--r--app/lib/feed_manager.rb2
-rw-r--r--app/lib/formatter.rb1
-rw-r--r--app/models/account.rb52
-rw-r--r--app/models/notification.rb27
-rw-r--r--app/models/status.rb2
-rw-r--r--app/models/tag.rb4
-rw-r--r--app/presenters/instance_presenter.rb28
-rw-r--r--app/services/fetch_remote_account_service.rb13
-rw-r--r--app/views/about/_registration.html.haml30
-rw-r--r--app/views/about/more.html.haml28
-rw-r--r--app/views/about/show.html.haml (renamed from app/views/about/index.html.haml)28
-rw-r--r--app/views/accounts/followers.html.haml2
-rw-r--r--app/views/accounts/following.html.haml2
-rw-r--r--app/views/accounts/show.html.haml2
-rw-r--r--app/views/admin/accounts/index.html.haml2
-rw-r--r--app/views/admin/domain_blocks/index.html.haml2
-rw-r--r--app/views/admin/pubsubhubbub/index.html.haml2
-rw-r--r--app/views/admin/reports/index.html.haml2
-rw-r--r--app/views/api/v1/accounts/show.rabl11
-rw-r--r--app/views/kaminari/_next_page.html.haml9
-rw-r--r--app/views/kaminari/_paginator.html.haml16
-rw-r--r--app/views/kaminari/_prev_page.html.haml9
-rw-r--r--app/views/shared/_landing_strip.html.haml5
-rw-r--r--app/views/stream_entries/_status.html.haml2
-rw-r--r--app/views/tags/show.html.haml2
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.html.erb2
-rw-r--r--app/views/user_mailer/confirmation_instructions.fr.text.erb2
-rw-r--r--app/workers/import_worker.rb4
74 files changed, 698 insertions, 525 deletions
diff --git a/app/assets/javascripts/components/actions/notifications.jsx b/app/assets/javascripts/components/actions/notifications.jsx
index 980b7d63e..11e814e1f 100644
--- a/app/assets/javascripts/components/actions/notifications.jsx
+++ b/app/assets/javascripts/components/actions/notifications.jsx
@@ -61,6 +61,8 @@ export function refreshNotifications() {
       params.since_id = ids.first().get('id');
     }
 
+    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
     api(getState).get('/api/v1/notifications', { params }).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
@@ -105,11 +107,11 @@ export function expandNotifications() {
 
     dispatch(expandNotificationsRequest());
 
-    api(getState).get(url, {
-      params: {
-        limit: 5
-      }
-    }).then(response => {
+    const params = {};
+
+    params.exclude_types = getState().getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
+
+    api(getState).get(url, params).then(response => {
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
diff --git a/app/assets/javascripts/components/components/account.jsx b/app/assets/javascripts/components/components/account.jsx
index 7a1c9f5ce..782cf382d 100644
--- a/app/assets/javascripts/components/components/account.jsx
+++ b/app/assets/javascripts/components/components/account.jsx
@@ -65,7 +65,7 @@ const Account = React.createClass({
       <div className='account'>
         <div style={{ display: 'flex' }}>
           <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
-            <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
+            <div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={36} /></div>
             <DisplayName account={account} />
           </Permalink>
 
diff --git a/app/assets/javascripts/components/components/avatar.jsx b/app/assets/javascripts/components/components/avatar.jsx
index 0237a1904..673b1a247 100644
--- a/app/assets/javascripts/components/components/avatar.jsx
+++ b/app/assets/javascripts/components/components/avatar.jsx
@@ -1,103 +1,18 @@
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 
-// From: http://stackoverflow.com/a/18320662
-const resample = (canvas, width, height, resize_canvas) => {
-  let width_source  = canvas.width;
-  let height_source = canvas.height;
-  width  = Math.round(width);
-  height = Math.round(height);
-
-  let ratio_w      = width_source / width;
-  let ratio_h      = height_source / height;
-  let ratio_w_half = Math.ceil(ratio_w / 2);
-  let ratio_h_half = Math.ceil(ratio_h / 2);
-
-  let ctx   = canvas.getContext("2d");
-  let img   = ctx.getImageData(0, 0, width_source, height_source);
-  let img2  = ctx.createImageData(width, height);
-  let data  = img.data;
-  let data2 = img2.data;
-
-  for (let j = 0; j < height; j++) {
-    for (let i = 0; i < width; i++) {
-      let x2            = (i + j * width) * 4;
-      let weight        = 0;
-      let weights       = 0;
-      let weights_alpha = 0;
-      let gx_r          = 0;
-      let gx_g          = 0;
-      let gx_b          = 0;
-      let gx_a          = 0;
-      let center_y      = (j + 0.5) * ratio_h;
-      let yy_start      = Math.floor(j * ratio_h);
-      let yy_stop       = Math.ceil((j + 1) * ratio_h);
-
-      for (let yy = yy_start; yy < yy_stop; yy++) {
-        let dy       = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
-        let center_x = (i + 0.5) * ratio_w;
-        let w0       = dy * dy; //pre-calc part of w
-        let xx_start = Math.floor(i * ratio_w);
-        let xx_stop  = Math.ceil((i + 1) * ratio_w);
-
-        for (let xx = xx_start; xx < xx_stop; xx++) {
-          let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
-          let w  = Math.sqrt(w0 + dx * dx);
-
-          if (w >= 1) {
-            // pixel too far
-            continue;
-          }
-
-          // hermite filter
-          weight    = 2 * w * w * w - 3 * w * w + 1;
-          let pos_x = 4 * (xx + yy * width_source);
-
-          // alpha
-          gx_a          += weight * data[pos_x + 3];
-          weights_alpha += weight;
-
-          // colors
-          if (data[pos_x + 3] < 255)
-            weight = weight * data[pos_x + 3] / 250;
-
-          gx_r    += weight * data[pos_x];
-          gx_g    += weight * data[pos_x + 1];
-          gx_b    += weight * data[pos_x + 2];
-          weights += weight;
-        }
-      }
-
-      data2[x2]     = gx_r / weights;
-      data2[x2 + 1] = gx_g / weights;
-      data2[x2 + 2] = gx_b / weights;
-      data2[x2 + 3] = gx_a / weights_alpha;
-    }
-  }
-
-  // clear and resize canvas
-  if (resize_canvas === true) {
-    canvas.width  = width;
-    canvas.height = height;
-  } else {
-    ctx.clearRect(0, 0, width_source, height_source);
-  }
-
-  // draw
-  ctx.putImageData(img2, 0, 0);
-};
-
 const Avatar = React.createClass({
 
   propTypes: {
     src: React.PropTypes.string.isRequired,
+    staticSrc: React.PropTypes.string,
     size: React.PropTypes.number.isRequired,
     style: React.PropTypes.object,
-    animated: React.PropTypes.bool
+    animate: React.PropTypes.bool
   },
 
   getDefaultProps () {
     return {
-      animated: true
+      animate: false
     };
   },
 
@@ -117,38 +32,30 @@ const Avatar = React.createClass({
     this.setState({ hovering: false });
   },
 
-  handleLoad () {
-    this.canvas.width  = this.image.naturalWidth;
-    this.canvas.height = this.image.naturalHeight;
-    this.canvas.getContext('2d').drawImage(this.image, 0, 0);
-
-    resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
-  },
-
-  setImageRef (c) {
-    this.image = c;
-  },
-
-  setCanvasRef (c) {
-    this.canvas = c;
-  },
-
   render () {
+    const { src, size, staticSrc, animate } = this.props;
     const { hovering } = this.state;
 
-    if (this.props.animated) {
-      return (
-        <div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
-          <img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
-        </div>
-      );
+    const style = {
+      ...this.props.style,
+      width: `${size}px`,
+      height: `${size}px`,
+      backgroundSize: `${size}px ${size}px`
+    };
+
+    if (hovering || animate) {
+      style.backgroundImage = `url(${src})`;
+    } else {
+      style.backgroundImage = `url(${staticSrc})`;
     }
 
     return (
-      <div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
-        <img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
-        <canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
-      </div>
+      <div
+        className='avatar'
+        onMouseEnter={this.handleMouseEnter}
+        onMouseLeave={this.handleMouseLeave}
+        style={style}
+      />
     );
   }
 
diff --git a/app/assets/javascripts/components/components/status.jsx b/app/assets/javascripts/components/components/status.jsx
index 60bf531e5..c4d5f829b 100644
--- a/app/assets/javascripts/components/components/status.jsx
+++ b/app/assets/javascripts/components/components/status.jsx
@@ -91,7 +91,7 @@ const Status = React.createClass({
 
           <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
             <div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
-              <Avatar src={status.getIn(['account', 'avatar'])} size={48} />
+              <Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
             </div>
 
             <DisplayName account={status.get('account')} />
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx
index 6c25afdea..9cf03bb32 100644
--- a/app/assets/javascripts/components/components/status_content.jsx
+++ b/app/assets/javascripts/components/components/status_content.jsx
@@ -36,6 +36,7 @@ const StatusContent = React.createClass({
 
       if (mention) {
         link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
+        link.setAttribute('title', mention.get('acct'));
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
       } else if (media) {
@@ -91,7 +92,7 @@ const StatusContent = React.createClass({
     const { status } = this.props;
     const { hidden } = this.state;
 
-    const content = { __html: emojify(status.get('content')) };
+    const content = { __html: emojify(status.get('content')).replace(/\n/g, '') };
     const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
     const directionStyle = { direction: 'ltr' };
 
@@ -125,7 +126,7 @@ const StatusContent = React.createClass({
           <div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
         </div>
       );
-    } else {
+    } else if (this.props.onClick) {
       return (
         <div
           className='status__content'
@@ -135,6 +136,14 @@ const StatusContent = React.createClass({
           dangerouslySetInnerHTML={content}
         />
       );
+    } else {
+      return (
+        <div
+          className='status__content'
+          style={{ ...directionStyle }}
+          dangerouslySetInnerHTML={content}
+        />
+      );
     }
   },
 
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 00f20074d..fea8b1594 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -48,6 +48,8 @@ import hu from 'react-intl/locale-data/hu';
 import uk from 'react-intl/locale-data/uk';
 import fi from 'react-intl/locale-data/fi';
 import eo from 'react-intl/locale-data/eo';
+import ru from 'react-intl/locale-data/ru';
+
 import getMessagesForLocale from '../locales';
 import { hydrateStore } from '../actions/store';
 import createStream from '../stream';
@@ -60,7 +62,9 @@ const browserHistory = useRouterHistory(createBrowserHistory)({
   basename: '/web'
 });
 
-addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo]);
+
+addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk, ...fi, ...eo, ...ru]);
+
 
 const Mastodon = React.createClass({
 
diff --git a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
index 5591b45cf..9e05193fb 100644
--- a/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
+++ b/app/assets/javascripts/components/features/compose/components/autosuggest_account.jsx
@@ -4,7 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 
 const AutosuggestAccount = ({ account }) => (
   <div style={{ overflow: 'hidden' }} className='autosuggest-account'>
-    <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
+    <div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={18} /></div>
     <DisplayName account={account} />
   </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 b016d3f28..cb4b62f6c 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -83,11 +83,23 @@ const ComposeForm = React.createClass({
     this.props.onChangeSpoilerText(e.target.value);
   },
 
+  componentWillReceiveProps (nextProps) {
+    // If this is the update where we've finished uploading,
+    // save the last caret position so we can restore it below!
+    if (!nextProps.is_uploading && this.props.is_uploading) {
+      this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
+    }
+  },
+
   componentDidUpdate (prevProps) {
-    if (this.props.focusDate !== prevProps.focusDate) {
-      // If replying to zero or one users, places the cursor at the end of the textbox.
-      // If replying to more than one user, selects any usernames past the first;
-      // this provides a convenient shortcut to drop everyone else from the conversation.
+    // This statement does several things: 
+    // - If we're beginning a reply, and,
+    //     - Replying to zero or one users, places the cursor at the end of the textbox.
+    //     - Replying to more than one user, selects any usernames past the first;
+    //       this provides a convenient shortcut to drop everyone else from the conversation.
+    // - If we've just finished uploading an image, and have a saved caret position,
+    //   restores the cursor to that position after the text changes!
+    if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
       let selectionEnd, selectionStart;
 
       if (this.props.preselectDate !== prevProps.preselectDate) {
@@ -118,7 +130,7 @@ const ComposeForm = React.createClass({
 
   render () {
     const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
-    const disabled = this.props.is_submitting || this.props.is_uploading;
+    const disabled = this.props.is_submitting;
 
     let publishText    = '';
     let privacyWarning = '';
diff --git a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
index 1920b29bf..fa577ce26 100644
--- a/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
+++ b/app/assets/javascripts/components/features/compose/components/emoji_picker_dropdown.jsx
@@ -47,7 +47,7 @@ const EmojiPickerDropdown = React.createClass({
         </DropdownTrigger>
 
         <DropdownContent className='dropdown__left'>
-          <EmojiPicker emojione={settings} onChange={this.handleChange} />
+          <EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
         </DropdownContent>
       </Dropdown>
     );
diff --git a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
index 076ac7cbb..1a748a23c 100644
--- a/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
+++ b/app/assets/javascripts/components/features/compose/components/navigation_bar.jsx
@@ -17,7 +17,7 @@ const NavigationBar = React.createClass({
   render () {
     return (
       <div className='navigation-bar'>
-        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
+        <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
 
         <div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
           <strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
diff --git a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
index a72bd32c2..11a89449e 100644
--- a/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
+++ b/app/assets/javascripts/components/features/compose/components/reply_indicator.jsx
@@ -50,7 +50,7 @@ const ReplyIndicator = React.createClass({
           <div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
 
           <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
-            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} /></div>
+            <div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
             <DisplayName account={status.get('account')} />
           </a>
         </div>
diff --git a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
index 1766655c2..9c713287c 100644
--- a/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
+++ b/app/assets/javascripts/components/features/follow_requests/components/account_authorize.jsx
@@ -33,7 +33,7 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
     <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>
+          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
           <DisplayName account={account} />
         </Permalink>
 
diff --git a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
index 62c3e61e0..debbfd01f 100644
--- a/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/clear_column_button.jsx
@@ -4,16 +4,6 @@ const messages = defineMessages({
   clear: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }
 });
 
-const iconStyle = {
-  fontSize: '16px',
-  padding: '15px',
-  position: 'absolute',
-  right: '48px',
-  top: '0',
-  cursor: 'pointer',
-  zIndex: '2'
-};
-
 const ClearColumnButton = React.createClass({
 
   propTypes: {
@@ -25,7 +15,7 @@ const ClearColumnButton = React.createClass({
     const { intl } = this.props;
 
     return (
-      <div title={intl.formatMessage(messages.clear)} className='column-icon' tabIndex='0' style={iconStyle} onClick={this.onClick}>
+      <div title={intl.formatMessage(messages.clear)} className='column-icon column-icon-clear' tabIndex='0' onClick={this.props.onClick}>
         <i className='fa fa-eraser' />
       </div>
     );
diff --git a/app/assets/javascripts/components/features/notifications/components/notification.jsx b/app/assets/javascripts/components/features/notifications/components/notification.jsx
index 0de4df52e..0607466d0 100644
--- a/app/assets/javascripts/components/features/notifications/components/notification.jsx
+++ b/app/assets/javascripts/components/features/notifications/components/notification.jsx
@@ -21,7 +21,7 @@ const Notification = React.createClass({
 
   renderFollow (account, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-follow'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-user-plus' />
@@ -41,7 +41,7 @@ const Notification = React.createClass({
 
   renderFavourite (notification, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-favourite'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-star' style={{ color: '#ca8f04' }} />
@@ -57,7 +57,7 @@ const Notification = React.createClass({
 
   renderReblog (notification, link) {
     return (
-      <div className='notification'>
+      <div className='notification notification-reblog'>
         <div className='notification__message'>
           <div style={{ position: 'absolute', 'left': '-26px'}}>
             <i className='fa fa-fw fa-retweet' />
@@ -76,7 +76,7 @@ const Notification = React.createClass({
     const account          = notification.get('account');
     const displayName      = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
     const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
-    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
+    const link             = <Permalink className='notification__display-name' style={linkStyle} href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
 
     switch(notification.get('type')) {
       case 'follow':
diff --git a/app/assets/javascripts/components/features/status/components/detailed_status.jsx b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
index caa46ff3c..2da57252e 100644
--- a/app/assets/javascripts/components/features/status/components/detailed_status.jsx
+++ b/app/assets/javascripts/components/features/status/components/detailed_status.jsx
@@ -54,7 +54,7 @@ const DetailedStatus = React.createClass({
     return (
       <div style={{ padding: '14px 10px' }} className='detailed-status'>
         <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
-          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} size={48} /></div>
+          <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} /></div>
           <DisplayName account={status.get('account')} />
         </a>
 
diff --git a/app/assets/javascripts/components/locales/fr.jsx b/app/assets/javascripts/components/locales/fr.jsx
index fdd9c0e00..568422ff3 100644
--- a/app/assets/javascripts/components/locales/fr.jsx
+++ b/app/assets/javascripts/components/locales/fr.jsx
@@ -14,6 +14,7 @@ const fr = {
   "status.show_less": "Replier",
   "status.open": "Déplier ce status",
   "status.report": "Signaler @{name}",
+  "status.load_more": "Charger plus",
   "video_player.toggle_sound": "Mettre/Couper le son",
   "account.mention": "Mentionner",
   "account.edit_profile": "Modifier le profil",
@@ -41,6 +42,7 @@ const fr = {
   "column.notifications": "Notifications",
   "column.blocks": "Utilisateurs bloqués",
   "column.favourites": "Favoris",
+  "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
   "tabs_bar.compose": "Composer",
   "tabs_bar.home": "Accueil",
   "tabs_bar.mentions": "Mentions",
diff --git a/app/assets/javascripts/components/locales/index.jsx b/app/assets/javascripts/components/locales/index.jsx
index 1e7b8b548..f9e1fe5bd 100644
--- a/app/assets/javascripts/components/locales/index.jsx
+++ b/app/assets/javascripts/components/locales/index.jsx
@@ -7,6 +7,8 @@ import pt from './pt';
 import uk from './uk';
 import fi from './fi';
 import eo from './eo';
+import ru from './ru';
+
 
 const locales = {
   en,
@@ -17,7 +19,9 @@ const locales = {
   pt,
   uk,
   fi,
-  eo
+  eo,
+  ru
+
 };
 
 export default function getMessagesForLocale (locale) {
diff --git a/app/assets/javascripts/components/locales/pt.jsx b/app/assets/javascripts/components/locales/pt.jsx
index d68724b13..8d1b88c75 100644
--- a/app/assets/javascripts/components/locales/pt.jsx
+++ b/app/assets/javascripts/components/locales/pt.jsx
@@ -2,54 +2,71 @@ const pt = {
   "column_back_button.label": "Voltar",
   "lightbox.close": "Fechar",
   "loading_indicator.label": "Carregando...",
-  "status.mention": "Menção",
-  "status.delete": "Deletar",
+  "status.mention": "Mencionar @{name}",
+  "status.delete": "Eliminar",
   "status.reply": "Responder",
-  "status.reblog": "Reblogar",
-  "status.favourite": "Favoritar",
-  "status.reblogged_by": "{name} reblogou",
-  "video_player.toggle_sound": "Alterar som",
-  "account.mention": "Menção",
+  "status.reblog": "Partilhar",
+  "status.favourite": "Adicionar aos favoritos",
+  "status.reblogged_by": "{name} partilhou",
+  "status.sensitive_warning": "Conteúdo sensível",
+  "status.sensitive_toggle": "Clique para ver",
+  "status.show_more": "Mostrar mais",
+  "status.show_less": "Mostrar menos",
+  "status.open": "Expandir",
+  "status.report": "Reportar @{name}",
+  "video_player.toggle_sound": "Ligar/Desligar som",
+  "account.mention": "Mencionar @{name}",
   "account.edit_profile": "Editar perfil",
-  "account.unblock": "Desbloquear",
-  "account.unfollow": "Unfollow",
-  "account.block": "Bloquear",
+  "account.unblock": "Não bloquear @{name}",
+  "account.unfollow": "Não seguir",
+  "account.block": "Bloquear @{name}",
   "account.follow": "Seguir",
-  "account.block": "Bloquear",
   "account.posts": "Posts",
   "account.follows": "Segue",
   "account.followers": "Seguidores",
-  "account.follows_you": "Segue você",
+  "account.follows_you": "É teu seguidor",
+  "account.requested": "A aguardar aprovação",
   "getting_started.heading": "Primeiros passos",
-  "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão entrando um endereço similar a e-mail no campo no topo da barra lateral.",
+  "getting_started.about_addressing": "Podes seguir pessoas se sabes o nome de usuário deles e o domínio em que estão colocando um endereço similar a e-mail no campo no topo da barra lateral.",
   "getting_started.about_shortcuts": "Se o usuário alvo está no mesmo domínio, só o nome funcionará. A mesma regra se aplica a mencionar pessoas nas postagens.",
-  "getting_started.about_developer": "O desenvolvedor desse projeto pode ser seguido em Gargron@mastodon.social",
+  "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}. {apps}.",
   "column.home": "Home",
-  "column.mentions": "Menções",
+  "column.community": "Local",
   "column.public": "Público",
-  "tabs_bar.compose": "Compôr",
+  "column.notifications": "Notificações",
+  "tabs_bar.compose": "Criar",
   "tabs_bar.home": "Home",
   "tabs_bar.mentions": "Menções",
   "tabs_bar.public": "Público",
   "tabs_bar.notifications": "Notificações",
-  "compose_form.placeholder": "Que estás pensando?",
+  "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
-  "compose_form.sensitive": "Marcar conteúdo como sensível",
-  "compose_form.unlisted": "Modo não-listado",
+  "compose_form.sensitive": "Media com conteúdo sensível",
+  "compose_form.spoiler": "Esconder texto com aviso",
+  "compose_form.private": "Tornar privado",
+  "compose_form.privacy_disclaimer": "O teu conteúdo privado vai ser partilhado com os utilizadores do {domains}. Confias {domainsCount, plural, one {neste servidor} other {nestes servidores}}? A privacidade só funciona em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não existem indicadores da privacidade da tua partilha, e podem ser partilhados com outros.",
+  "compose_form.unlisted": "Não mostrar na listagem pública",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.preferences": "Preferências",
-  "navigation_bar.public_timeline": "Timeline Pública",
-  "navigation_bar.logout": "Logout",
+  "navigation_bar.community_timeline": "Local",
+  "navigation_bar.public_timeline": "Público",
+  "navigation_bar.logout": "Sair",
   "reply_indicator.cancel": "Cancelar",
-  "search.placeholder": "Busca",
+  "search.placeholder": "Pesquisar",
   "search.account": "Conta",
   "search.hashtag": "Hashtag",
   "upload_button.label": "Adicionar media",
-  "upload_form.undo": "Desfazer",
-  "notification.follow": "{name} seguiu você",
-  "notification.favourite": "{name} favoritou  seu post",
-  "notification.reblog": "{name} reblogou o seu post",
-  "notification.mention": "{name} mecionou você"
+  "upload_form.undo": "Anular",
+  "notification.follow": "{name} seguiu-te",
+  "notification.favourite": "{name} adicionou o teu post aos favoritos",
+  "notification.reblog": "{name} partilhou o teu post",
+  "notification.mention": "{name} mencionou-te",
+  "notifications.column_settings.alert": "Notificações no computador",
+  "notifications.column_settings.show": "Mostrar nas colunas",
+  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.favourite": "Favoritos:",
+  "notifications.column_settings.mention": "Menções:",
+  "notifications.column_settings.reblog": "Partilhas:",
 };
 
 export default pt;
diff --git a/app/assets/javascripts/components/locales/ru.jsx b/app/assets/javascripts/components/locales/ru.jsx
new file mode 100644
index 000000000..e109005a7
--- /dev/null
+++ b/app/assets/javascripts/components/locales/ru.jsx
@@ -0,0 +1,68 @@
+const ru = {
+  "column_back_button.label": "Назад",
+  "lightbox.close": "Закрыть",
+  "loading_indicator.label": "Загрузка...",
+  "status.mention": "Упомянуть @{name}",
+  "status.delete": "Удалить",
+  "status.reply": "Ответить",
+  "status.reblog": "Продвинуть",
+  "status.favourite": "Нравится",
+  "status.reblogged_by": "{name} продвинул(а)",
+  "status.sensitive_warning": "Чувствительный контент",
+  "status.sensitive_toggle": "Нажмите для просмотра",
+  "video_player.toggle_sound": "Вкл./выкл. звук",
+  "account.mention": "Упомянуть @{name}",
+  "account.edit_profile": "Изменить профиль",
+  "account.unblock": "Разблокировать @{name}",
+  "account.unfollow": "Отписаться",
+  "account.block": "Блокировать @{name}",
+  "account.follow": "Подписаться",
+  "account.posts": "Посты",
+  "account.follows": "Подписки",
+  "account.followers": "Подписчики",
+  "account.follows_you": "Подписан(а) на Вас",
+  "account.requested": "Ожидает подтверждения",
+  "getting_started.heading": "Добро пожаловать",
+  "getting_started.about_addressing": "Вы можете подписаться на человека, зная имя пользователя и домен, на котором он находится, введя e-mail-подобный адрес в форму поиска.",
+  "getting_started.about_shortcuts": "Если пользователь находится на одном с Вами домене, можно использовать только имя. То же правило применимо к упоминанию пользователей в статусах.",
+  "getting_started.open_source_notice": "Mastodon - программа с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}. {apps}.",
+  "column.home": "Главная",
+  "column.community": "Локальная лента",
+  "column.public": "Глобальная лента",
+  "column.notifications": "Уведомления",
+  "tabs_bar.compose": "Написать",
+  "tabs_bar.home": "Главная",
+  "tabs_bar.mentions": "Упоминания",
+  "tabs_bar.public": "Глобальная лента",
+  "tabs_bar.notifications": "Уведомления",
+  "compose_form.placeholder": "О чем Вы думаете?",
+  "compose_form.publish": "Протрубить",
+  "compose_form.sensitive": "Отметить как чувствительный контент",
+  "compose_form.spoiler": "Скрыть текст за предупреждением",
+  "compose_form.private": "Отметить как приватное",
+  "compose_form.privacy_disclaimer": "Ваш приватный статус будет доставлен упомянутым пользователям на доменах {domains}. Доверяете ли вы {domainsCount, plural, one {этому серверу} other {этим серверам}}? Приватность постов работает только на узлах Mastodon. Если {domains} {domainsCount, plural, one {не является узлом Mastodon} other {не являются узлами Mastodon}}, приватность поста не будет указана, и он может оказаться продвинут или иным образом показан не обозначенным Вами пользователям.",
+  "compose_form.unlisted": "Не отображать в публичных лентах",
+  "navigation_bar.edit_profile": "Изменить профиль",
+  "navigation_bar.preferences": "Опции",
+  "navigation_bar.community_timeline": "Локальная лента",
+  "navigation_bar.public_timeline": "Глобальная лента",
+  "navigation_bar.logout": "Выйти",
+  "reply_indicator.cancel": "Отмена",
+  "search.placeholder": "Поиск",
+  "search.account": "Аккаунт",
+  "search.hashtag": "Хэштег",
+  "upload_button.label": "Добавить медиаконтент",
+  "upload_form.undo": "Отменить",
+  "notification.follow": "{name} подписался(-лась) на Вас",
+  "notification.favourite": "{name} понравился Ваш статус",
+  "notification.reblog": "{name} продвинул(а) Ваш статус",
+  "notification.mention": "{name} упомянул(а) Вас",
+  "notifications.column_settings.alert": "Десктопные уведомления",
+  "notifications.column_settings.show": "Показывать в колонке",
+  "notifications.column_settings.follow": "Новые подписчики:",
+  "notifications.column_settings.favourite": "Нравится:",
+  "notifications.column_settings.mention": "Упоминания:",
+  "notifications.column_settings.reblog": "Продвижения:",
+};
+
+export default ru;
diff --git a/app/assets/stylesheets/accounts.scss b/app/assets/stylesheets/accounts.scss
index b3ae33500..50181d86e 100644
--- a/app/assets/stylesheets/accounts.scss
+++ b/app/assets/stylesheets/accounts.scss
@@ -72,6 +72,7 @@
     position: relative;
     z-index: 2;
     flex-direction: row;
+    background: rgba(0,0,0,0.5);
   }
 
   .details-counters {
@@ -83,7 +84,7 @@
   .counter {
     width: 80px;
     color: $color3;
-    padding: 0 10px;
+    padding: 5px 10px 0px;
     margin-bottom: 10px;
     border-right: 1px solid $color3;
     cursor: default;
@@ -173,7 +174,7 @@
   text-align: center;
   overflow: hidden;
 
-  a, .current, .next_page, .previous_page, .gap {
+  a, .current, .page, .gap {
     font-size: 14px;
     color: $color5;
     font-weight: 500;
@@ -193,12 +194,12 @@
     cursor: default;
   }
 
-  .previous_page, .next_page {
+  .prev, .next {
     text-transform: uppercase;
     color: $color2;
   }
 
-  .previous_page {
+  .prev {
     float: left;
     padding-left: 0;
 
@@ -208,7 +209,7 @@
     }
   }
 
-  .next_page {
+  .next {
     float: right;
     padding-right: 0;
 
@@ -226,11 +227,11 @@
   @media screen and (max-width: 360px) {
     padding: 30px 20px;
 
-    a, .current, .next_page, .previous_page, .gap {
+    a, .current, .next, .prev, .gap {
       display: none;
     }
 
-    .next_page, .previous_page {
+    .next, .prev {
       display: inline-block;
     }
   }
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index d31f148a2..316398874 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -1,7 +1,7 @@
 @import 'variables';
 
 .app-body{
- -ms-overflow-style: -ms-autohiding-scrollbar; 
+ -ms-overflow-style: -ms-autohiding-scrollbar;
 }
 
 .button {
@@ -49,6 +49,22 @@
   }
 }
 
+.column-icon-clear {
+  font-size: 16px;
+  padding: 15px;
+  position: absolute;
+  right: 48px;
+  top: 0;
+  cursor: pointer;
+  z-index: 2;
+}
+
+@media screen and (min-width: 1024px) {
+  .column-icon-clear {
+    top: 10px;
+  }
+}
+
 .icon-button {
   display: inline-block;
   padding: 0;
@@ -149,6 +165,14 @@
   }
 }
 
+.avatar {
+  border-radius: 4px;
+  background: transparent no-repeat;
+  background-position: 50%;
+  background-clip: padding-box;
+  position: relative;
+}
+
 .lightbox .icon-button {
   color: $color1;
 }
@@ -714,7 +738,7 @@ a.status__content__spoiler-link {
 
 @media screen and (min-width: 360px) {
   .columns-area {
-    margin: 10px;
+    padding: 10px;
   }
 }
 
@@ -722,9 +746,12 @@ a.status__content__spoiler-link {
   width: 330px;
   position: relative;
   box-sizing: border-box;
-  background: $color1;
   display: flex;
   flex-direction: column;
+
+  > .scrollable {
+    background: $color1;
+  }
 }
 
 .ui {
@@ -756,6 +783,58 @@ a.status__content__spoiler-link {
   border-bottom: 2px solid transparent;
 }
 
+.column, .drawer {
+  flex: 1 1 100%;
+  overflow: hidden;
+}
+
+@media screen and (min-width: 360px) {
+  .tabs-bar {
+    margin: 10px;
+    margin-bottom: 0;
+  }
+
+  .search {
+    margin-bottom: 10px;
+  }
+}
+
+@media screen and (max-width: 1024px) {
+  .column, .drawer {
+    width: 100%;
+    padding: 0;
+  }
+
+  .columns-area {
+    flex-direction: column;
+  }
+
+  .search__input, .autosuggest-textarea__textarea {
+    font-size: 16px;
+  }
+}
+
+@media screen and (min-width: 1024px) {
+  .columns-area {
+    padding: 0;
+  }
+
+  .column, .drawer {
+    flex: 0 0 auto;
+    padding: 10px;
+    padding-left: 5px;
+    padding-right: 5px;
+
+    &:first-child {
+      padding-left: 10px;
+    }
+
+    &:last-child {
+      padding-right: 10px;
+    }
+  }
+}
+
 @media screen and (min-width: 2560px) {
   .columns-area {
     justify-content: center;
@@ -815,37 +894,6 @@ a.status__content__spoiler-link {
   }
 }
 
-.column, .drawer {
-  margin-left: 5px;
-  margin-right: 5px;
-  flex: 0 0 auto;
-  overflow: hidden;
-}
-
-.column:first-child, .drawer:first-child {
-  margin-left: 0;
-}
-
-.column:last-child, .drawer:last-child {
-  margin-right: 0;
-}
-
-@media screen and (max-width: 1024px) {
-  .column, .drawer {
-    width: 100%;
-    margin: 0;
-    flex: 1 1 100%;
-  }
-
-  .columns-area {
-    flex-direction: column;
-  }
-
-  .search__input, .autosuggest-textarea__textarea {
-    font-size: 16px;
-  }
-}
-
 .tabs-bar {
   display: flex;
   background: lighten($color1, 8%);
@@ -856,17 +904,18 @@ a.status__content__spoiler-link {
 .tabs-bar__link {
   display: block;
   flex: 1 1 auto;
-  padding: 10px 5px;
+  padding: 15px 10px;
   color: $color5;
   text-decoration: none;
   text-align: center;
-  font-size:12px;
+  font-size: 14px;
   font-weight: 500;
   border-bottom: 2px solid lighten($color1, 8%);
   transition: all 200ms linear;
 
   .fa {
     font-weight: 400;
+    font-size: 16px;
   }
 
   &.active {
@@ -880,27 +929,13 @@ a.status__content__spoiler-link {
   }
 
   span {
+    margin-left: 5px;
     display: none;
   }
 }
 
-@media screen and (min-width: 360px) {
-  .tabs-bar {
-    margin: 10px;
-    margin-bottom: 0;
-  }
-
-  .search {
-    margin-bottom: 10px;
-  }
-}
-
 @media screen and (min-width: 600px) {
   .tabs-bar__link {
-    .fa {
-      margin-right: 5px;
-    }
-
     span {
       display: inline;
     }
@@ -1362,12 +1397,15 @@ button.icon-button.active i.fa-retweet {
 
 .empty-column-indicator {
   color: lighten($color1, 20%);
+  background: $color1;
   text-align: center;
   padding: 20px;
-  padding-top: 100px;
   font-size: 15px;
   font-weight: 400;
   cursor: default;
+  display: flex;
+  flex: 1 1 auto;
+  align-items: center;
 
   a {
     color: $color4;
@@ -1395,7 +1433,7 @@ button.icon-button.active i.fa-retweet {
 .emoji-dialog {
   width: 280px;
   height: 220px;
-  background: $color2;
+  background: darken($color3, 10%);
   box-sizing: border-box;
   border-radius: 2px;
   overflow: hidden;
@@ -1404,6 +1442,8 @@ button.icon-button.active i.fa-retweet {
 
   .emojione {
     margin: 0;
+    width: 100%;
+    height: auto;
   }
 
   .emoji-dialog-header {
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 7fd43489f..04e7ddacf 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -2,30 +2,25 @@
 
 class AboutController < ApplicationController
   before_action :set_body_classes
+  before_action :set_instance_presenter, only: [:show, :more]
 
-  def index
-    @description                  = Setting.site_description
-    @open_registrations           = Setting.open_registrations
-    @closed_registrations_message = Setting.closed_registrations_message
+  def show; end
 
-    @user = User.new
-    @user.build_account
-  end
-
-  def more
-    @description          = Setting.site_description
-    @extended_description = Setting.site_extended_description
-    @contact_account      = Account.find_local(Setting.site_contact_username)
-    @contact_email        = Setting.site_contact_email
-    @user_count           = Rails.cache.fetch('user_count')            { User.count }
-    @status_count         = Rails.cache.fetch('local_status_count')    { Status.local.count }
-    @domain_count         = Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
-  end
+  def more; end
 
   def terms; end
 
   private
 
+  def new_user
+    User.new.tap(&:build_account)
+  end
+  helper_method :new_user
+
+  def set_instance_presenter
+    @instance_presenter = InstancePresenter.new
+  end
+
   def set_body_classes
     @body_classes = 'about-body'
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 619c04be2..d4f157614 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -35,11 +35,11 @@ class AccountsController < ApplicationController
   end
 
   def followers
-    @followers = @account.followers.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
+    @followers = @account.followers.order('follows.created_at desc').page(params[:page]).per(12)
   end
 
   def following
-    @following = @account.following.order('follows.created_at desc').paginate(page: params[:page], per_page: 12)
+    @following = @account.following.order('follows.created_at desc').page(params[:page]).per(12)
   end
 
   private
@@ -53,7 +53,7 @@ class AccountsController < ApplicationController
   end
 
   def webfinger_account_url
-    webfinger_url(resource: "acct:#{@account.acct}@#{Rails.configuration.x.local_domain}")
+    webfinger_url(resource: @account.to_webfinger_s)
   end
 
   def check_account_suspension
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index df2c7bebf..71cb8edd8 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -1,51 +1,50 @@
 # frozen_string_literal: true
 
-class Admin::AccountsController < ApplicationController
-  before_action :require_admin!
-  before_action :set_account, except: :index
-
-  layout 'admin'
-
-  def index
-    @accounts = Account.alphabetic.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.silenced                          if params[:silenced].present?
-    @accounts = @accounts.recent                            if params[:recent].present?
-    @accounts = @accounts.suspended                         if params[:suspended].present?
-  end
-
-  def show; end
-
-  def suspend
-    Admin::SuspensionWorker.perform_async(@account.id)
-    redirect_to admin_accounts_path
-  end
-
-  def unsuspend
-    @account.update(suspended: false)
-    redirect_to admin_accounts_path
-  end
-
-  def silence
-    @account.update(silenced: true)
-    redirect_to admin_accounts_path
-  end
-
-  def unsilence
-    @account.update(silenced: false)
-    redirect_to admin_accounts_path
-  end
-
-  private
-
-  def set_account
-    @account = Account.find(params[:id])
-  end
-
-  def account_params
-    params.require(:account).permit(:silenced, :suspended)
+module Admin
+  class AccountsController < BaseController
+    before_action :set_account, except: :index
+
+    def index
+      @accounts = Account.alphabetic.page(params[:page])
+
+      @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.silenced                          if params[:silenced].present?
+      @accounts = @accounts.recent                            if params[:recent].present?
+      @accounts = @accounts.suspended                         if params[:suspended].present?
+    end
+
+    def show; end
+
+    def suspend
+      Admin::SuspensionWorker.perform_async(@account.id)
+      redirect_to admin_accounts_path
+    end
+
+    def unsuspend
+      @account.update(suspended: false)
+      redirect_to admin_accounts_path
+    end
+
+    def silence
+      @account.update(silenced: true)
+      redirect_to admin_accounts_path
+    end
+
+    def unsilence
+      @account.update(silenced: false)
+      redirect_to admin_accounts_path
+    end
+
+    private
+
+    def set_account
+      @account = Account.find(params[:id])
+    end
+
+    def account_params
+      params.require(:account).permit(:silenced, :suspended)
+    end
   end
 end
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
new file mode 100644
index 000000000..11fe326bc
--- /dev/null
+++ b/app/controllers/admin/base_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Admin
+  class BaseController < ApplicationController
+    before_action :require_admin!
+
+    layout 'admin'
+  end
+end
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 1f4432847..a8b56c085 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -1,32 +1,30 @@
 # frozen_string_literal: true
 
-class Admin::DomainBlocksController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
-
-  def index
-    @blocks = DomainBlock.paginate(page: params[:page], per_page: 40)
-  end
+module Admin
+  class DomainBlocksController < BaseController
+    def index
+      @blocks = DomainBlock.page(params[:page])
+    end
 
-  def new
-    @domain_block = DomainBlock.new
-  end
+    def new
+      @domain_block = DomainBlock.new
+    end
 
-  def create
-    @domain_block = DomainBlock.new(resource_params)
+    def create
+      @domain_block = DomainBlock.new(resource_params)
 
-    if @domain_block.save
-      DomainBlockWorker.perform_async(@domain_block.id)
-      redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
-    else
-      render action: :new
+      if @domain_block.save
+        DomainBlockWorker.perform_async(@domain_block.id)
+        redirect_to admin_domain_blocks_path, notice: 'Domain block is now being processed'
+      else
+        render action: :new
+      end
     end
-  end
 
-  private
+    private
 
-  def resource_params
-    params.require(:domain_block).permit(:domain, :severity)
+    def resource_params
+      params.require(:domain_block).permit(:domain, :severity)
+    end
   end
 end
diff --git a/app/controllers/admin/pubsubhubbub_controller.rb b/app/controllers/admin/pubsubhubbub_controller.rb
index b9e840ffe..31c80a174 100644
--- a/app/controllers/admin/pubsubhubbub_controller.rb
+++ b/app/controllers/admin/pubsubhubbub_controller.rb
@@ -1,11 +1,9 @@
 # frozen_string_literal: true
 
-class Admin::PubsubhubbubController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
-
-  def index
-    @subscriptions = Subscription.order('id desc').includes(:account).paginate(page: params[:page], per_page: 40)
+module Admin
+  class PubsubhubbubController < BaseController
+    def index
+      @subscriptions = Subscription.order('id desc').includes(:account).page(params[:page])
+    end
   end
 end
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index 2b3b1809f..3c3082318 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -1,45 +1,44 @@
 # frozen_string_literal: true
 
-class Admin::ReportsController < ApplicationController
-  before_action :require_admin!
-  before_action :set_report, except: [:index]
-
-  layout 'admin'
-
-  def index
-    @reports = Report.includes(:account, :target_account).order('id desc').paginate(page: params[:page], per_page: 40)
-    @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
-  end
-
-  def show
-    @statuses = Status.where(id: @report.status_ids)
-  end
-
-  def resolve
-    @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def suspend
-    Admin::SuspensionWorker.perform_async(@report.target_account.id)
-    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def silence
-    @report.target_account.update(silenced: true)
-    Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
-    redirect_to admin_report_path(@report)
-  end
-
-  def remove
-    RemovalWorker.perform_async(params[:status_id])
-    redirect_to admin_report_path(@report)
-  end
-
-  private
-
-  def set_report
-    @report = Report.find(params[:id])
+module Admin
+  class ReportsController < BaseController
+    before_action :set_report, except: [:index]
+
+    def index
+      @reports = Report.includes(:account, :target_account).order('id desc').page(params[:page])
+      @reports = params[:action_taken].present? ? @reports.resolved : @reports.unresolved
+    end
+
+    def show
+      @statuses = Status.where(id: @report.status_ids)
+    end
+
+    def resolve
+      @report.update(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def suspend
+      Admin::SuspensionWorker.perform_async(@report.target_account.id)
+      Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def silence
+      @report.target_account.update(silenced: true)
+      Report.unresolved.where(target_account: @report.target_account).update_all(action_taken: true, action_taken_by_account_id: current_account.id)
+      redirect_to admin_report_path(@report)
+    end
+
+    def remove
+      RemovalWorker.perform_async(params[:status_id])
+      redirect_to admin_report_path(@report)
+    end
+
+    private
+
+    def set_report
+      @report = Report.find(params[:id])
+    end
   end
 end
diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb
index 7615c781d..6cca5c3e3 100644
--- a/app/controllers/admin/settings_controller.rb
+++ b/app/controllers/admin/settings_controller.rb
@@ -1,35 +1,33 @@
 # frozen_string_literal: true
 
-class Admin::SettingsController < ApplicationController
-  before_action :require_admin!
-
-  layout 'admin'
+module Admin
+  class SettingsController < BaseController
+    def index
+      @settings = Setting.all_as_records
+    end
 
-  def index
-    @settings = Setting.all_as_records
-  end
+    def update
+      @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
+      value    = settings_params[:value]
 
-  def update
-    @setting = Setting.where(var: params[:id]).first_or_initialize(var: params[:id])
-    value    = settings_params[:value]
+      # Special cases
+      value = value == 'true' if @setting.var == 'open_registrations'
 
-    # Special cases
-    value = value == 'true' if @setting.var == 'open_registrations'
+      if @setting.value != value
+        @setting.value = value
+        @setting.save
+      end
 
-    if @setting.value != value
-      @setting.value = value
-      @setting.save
+      respond_to do |format|
+        format.html { redirect_to admin_settings_path }
+        format.json { respond_with_bip(@setting) }
+      end
     end
 
-    respond_to do |format|
-      format.html { redirect_to admin_settings_path }
-      format.json { respond_with_bip(@setting) }
-    end
-  end
-
-  private
+    private
 
-  def settings_params
-    params.require(:setting).permit(:value)
+    def settings_params
+      params.require(:setting).permit(:value)
+    end
   end
 end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 454873116..2c44e36a7 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -1,10 +1,11 @@
 # frozen_string_literal: true
 
 class Api::V1::AccountsController < ApiController
-  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock, :mute, :unmute, :update_credentials]
   before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock, :mute, :unmute]
+  before_action -> { doorkeeper_authorize! :write }, only: [:update_credentials]
   before_action :require_user!, except: [:show, :following, :followers, :statuses]
-  before_action :set_account, except: [:verify_credentials, :suggestions, :search]
+  before_action :set_account, except: [:verify_credentials, :update_credentials, :suggestions, :search]
 
   respond_to :json
 
@@ -15,6 +16,12 @@ class Api::V1::AccountsController < ApiController
     render action: :show
   end
 
+  def update_credentials
+    current_account.update!(account_params)
+    @account = current_account
+    render action: :show
+  end
+
   def following
     results   = Follow.where(account: @account).paginate_by_max_id(limit_param(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
@@ -135,4 +142,8 @@ class Api::V1::AccountsController < ApiController
   def statuses_pagination_params(core_params)
     params.permit(:limit, :only_media, :exclude_replies).merge(core_params)
   end
+
+  def account_params
+    params.permit(:display_name, :note, :avatar, :header)
+  end
 end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index 71c054334..3cff29982 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::NotificationsController < ApiController
   DEFAULT_NOTIFICATIONS_LIMIT = 15
 
   def index
-    @notifications = Notification.where(account: current_account).browserable.paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
+    @notifications = Notification.where(account: current_account).browserable(exclude_types).paginate_by_max_id(limit_param(DEFAULT_NOTIFICATIONS_LIMIT), params[:max_id], params[:since_id])
     @notifications = cache_collection(@notifications, Notification)
     statuses       = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
 
@@ -32,7 +32,13 @@ class Api::V1::NotificationsController < ApiController
 
   private
 
+  def exclude_types
+    val = params.permit(exclude_types: [])[:exclude_types] || []
+    val = [val] unless val.is_a?(Enumerable)
+    val
+  end
+
   def pagination_params(core_params)
-    params.permit(:limit).merge(core_params)
+    params.permit(:limit, exclude_types: []).merge(core_params)
   end
 end
diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb
index db16f82e5..57604f1dc 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -7,6 +7,7 @@ class ApiController < ApplicationController
   protect_from_forgery with: :null_session
 
   skip_before_action :verify_authenticity_token
+  skip_before_action :store_current_location
 
   before_action :set_rate_limit_headers
 
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index 6528ce45e..bcf3fd0a0 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -26,6 +26,8 @@ module Localized
   end
 
   def default_locale
-    ENV.fetch('DEFAULT_LOCALE') { I18n.default_locale }
+    ENV.fetch('DEFAULT_LOCALE') { 
+      http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
+    }
   end
 end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 1e3f786ec..22e376836 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -25,7 +25,7 @@ class RemoteFollowController < ApplicationController
 
       session[:remote_follow] = @remote_follow.acct
 
-      redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: "#{@account.username}@#{Rails.configuration.x.local_domain}").to_s
+      redirect_to Addressable::Template.new(redirect_url_link.template).expand(uri: @account.to_webfinger_s).to_s
     else
       render :new
     end
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index 4fcec5322..ff688978c 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -39,7 +39,7 @@ class Settings::ExportsController < ApplicationController
   def accounts_list_to_csv(list)
     CSV.generate do |csv|
       list.each do |account|
-        csv << [(account.local? ? "#{account.username}@#{Rails.configuration.x.local_domain}" : account.acct)]
+        csv << [(account.local? ? account.local_username_and_domain : account.acct)]
       end
     end
   end
diff --git a/app/controllers/xrd_controller.rb b/app/controllers/xrd_controller.rb
index 6db87cefc..5964172e9 100644
--- a/app/controllers/xrd_controller.rb
+++ b/app/controllers/xrd_controller.rb
@@ -14,7 +14,7 @@ class XrdController < ApplicationController
 
   def webfinger
     @account = Account.find_local!(username_from_resource)
-    @canonical_account_uri = "acct:#{@account.username}@#{Rails.configuration.x.local_domain}"
+    @canonical_account_uri = @account.to_webfinger_s
     @magic_key = pem_to_magic_key(@account.keypair.public_key)
 
     respond_to do |format|
diff --git a/app/helpers/about_helper.rb b/app/helpers/about_helper.rb
deleted file mode 100644
index 0f57a7b5e..000000000
--- a/app/helpers/about_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module AboutHelper
-end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
deleted file mode 100644
index af23a78d1..000000000
--- a/app/helpers/accounts_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module AccountsHelper
-  def pagination_options
-    {
-      previous_label: safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '),
-      next_label: safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '),
-      inner_window: 1,
-      outer_window: 0,
-    }
-  end
-end
diff --git a/app/helpers/admin/domain_blocks_helper.rb b/app/helpers/admin/domain_blocks_helper.rb
deleted file mode 100644
index d66c8d5e1..000000000
--- a/app/helpers/admin/domain_blocks_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module Admin::DomainBlocksHelper
-end
diff --git a/app/helpers/admin/pubsubhubbub_helper.rb b/app/helpers/admin/pubsubhubbub_helper.rb
deleted file mode 100644
index c2fc2e7da..000000000
--- a/app/helpers/admin/pubsubhubbub_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module Admin::PubsubhubbubHelper
-end
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index b750eeb07..185388ec9 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -160,7 +160,7 @@ module AtomBuilderHelper
     object_type      xml, :person
     uri              xml, TagManager.instance.uri_for(account)
     name             xml, account.username
-    email            xml, account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct
+    email            xml, account.local? ? account.local_username_and_domain : account.acct
     summary          xml, account.note
     link_alternate   xml, TagManager.instance.url_for(account)
     link_avatar      xml, account
diff --git a/app/helpers/authorize_follow_helper.rb b/app/helpers/authorize_follow_helper.rb
deleted file mode 100644
index 99ee03c2f..000000000
--- a/app/helpers/authorize_follow_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module AuthorizeFollowHelper
-end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 74dc0e11d..327ca4e98 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -5,13 +5,15 @@ module SettingsHelper
     en: 'English',
     de: 'Deutsch',
     es: 'Español',
+    eo: 'Esperanto',
     pt: 'Português',
     fr: 'Français',
     hu: 'Magyar',
     uk: 'Українська',
     'zh-CN': '简体中文',
     fi: 'Suomi',
-    eo: 'Esperanto',
+    ru: 'Русский',
+
   }.freeze
 
   def human_locale(locale)
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
deleted file mode 100644
index 5b2b3ca59..000000000
--- a/app/helpers/tags_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module TagsHelper
-end
diff --git a/app/helpers/xrd_helper.rb b/app/helpers/xrd_helper.rb
deleted file mode 100644
index 2281a0278..000000000
--- a/app/helpers/xrd_helper.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module XrdHelper
-end
diff --git a/app/lib/atom_serializer.rb b/app/lib/atom_serializer.rb
index b9dcee6b3..68d2fce68 100644
--- a/app/lib/atom_serializer.rb
+++ b/app/lib/atom_serializer.rb
@@ -20,7 +20,7 @@ class AtomSerializer
     append_element(author, 'activity:object-type', TagManager::TYPES[:person])
     append_element(author, 'uri', uri)
     append_element(author, 'name', account.username)
-    append_element(author, 'email', account.local? ? "#{account.acct}@#{Rails.configuration.x.local_domain}" : account.acct)
+    append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
     append_element(author, 'summary', account.note)
     append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
     append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original)))
@@ -67,7 +67,7 @@ class AtomSerializer
     append_element(entry, 'id', TagManager.instance.unique_tag(stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type))
     append_element(entry, 'published', stream_entry.created_at.iso8601)
     append_element(entry, 'updated', stream_entry.updated_at.iso8601)
-    append_element(entry, 'title', stream_entry&.status&.title)
+    append_element(entry, 'title', stream_entry&.status&.title || 'Delete')
 
     entry << author(stream_entry.account) if root
 
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 58d9fb1fc..339a5c78b 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -66,7 +66,7 @@ class FeedManager
     timeline_key = key(:home, into_account.id)
     oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 
-    from_account.statuses.select('id').where('id > ?', oldest_home_score).find_in_batches do |statuses|
+    from_account.statuses.select('id').where('id > ?', oldest_home_score).reorder(nil).find_in_batches do |statuses|
       redis.pipelined do
         statuses.each do |status|
           redis.zrem(timeline_key, status.id)
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index da7ad2027..c3f331ff7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -15,7 +15,6 @@ class Formatter
     html = status.text
     html = encode(html)
     html = simple_format(html, {}, sanitize: false)
-    html = html.gsub(/\n/, '')
     html = link_urls(html)
     html = link_mentions(html, status.mentions)
     html = link_hashtags(html)
diff --git a/app/models/account.rb b/app/models/account.rb
index cbba8b5b6..8ceda7f97 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -12,12 +12,12 @@ class Account < ApplicationRecord
   validates :username, presence: true, uniqueness: { scope: :domain, case_sensitive: true }, unless: 'local?'
 
   # Avatar upload
-  has_attached_file :avatar, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, 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: { original: '700x335#' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' }
   validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :header, less_than: 2.megabytes
 
@@ -120,6 +120,14 @@ class Account < ApplicationRecord
     local? ? username : "#{username}@#{domain}"
   end
 
+  def local_username_and_domain
+    "#{username}@#{Rails.configuration.x.local_domain}"
+  end
+
+  def to_webfinger_s
+    "acct:#{local_username_and_domain}"
+  end
+
   def subscribed?
     !subscription_expires_at.blank?
   end
@@ -150,6 +158,22 @@ class Account < ApplicationRecord
     save!
   end
 
+  def avatar_original_url
+    avatar.url(:original)
+  end
+
+  def avatar_static_url
+    avatar_content_type == 'image/gif' ? avatar.url(:static) : avatar_original_url
+  end
+
+  def header_original_url
+    header.url(:original)
+  end
+
+  def header_static_url
+    header_content_type == 'image/gif' ? header.url(:static) : header_original_url
+  end
+
   def avatar_remote_url=(url)
     parsed_url = URI.parse(url)
 
@@ -203,7 +227,7 @@ class Account < ApplicationRecord
     end
 
     def triadic_closures(account, limit = 5)
-      sql = <<SQL
+      sql = <<-SQL.squish
         WITH first_degree AS (
             SELECT target_account_id
             FROM follows
@@ -216,7 +240,7 @@ class Account < ApplicationRecord
         GROUP BY target_account_id, accounts.id
         ORDER BY count(account_id) DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, account.id, account.id, limit])
     end
@@ -226,7 +250,7 @@ SQL
       textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           accounts.*,
           ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
@@ -234,7 +258,7 @@ SQL
         WHERE #{query} @@ #{textsearch}
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, limit])
     end
@@ -244,7 +268,7 @@ SQL
       textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           accounts.*,
           (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
@@ -254,7 +278,7 @@ SQL
         GROUP BY accounts.id
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Account.find_by_sql([sql, account.id, account.id, limit])
     end
@@ -284,6 +308,18 @@ SQL
     def follow_mapping(query, field)
       query.pluck(field).inject({}) { |mapping, id| mapping[id] = true; mapping }
     end
+
+    def avatar_styles(file)
+      styles = { original: '120x120#' }
+      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles
+    end
+
+    def header_styles(file)
+      styles = { original: '700x335#' }
+      styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
+      styles
+    end
   end
 
   before_create do
diff --git a/app/models/notification.rb b/app/models/notification.rb
index b7b474869..302d4382d 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -16,10 +16,17 @@ class Notification < ApplicationRecord
 
   validates :account_id, uniqueness: { scope: [:activity_type, :activity_id] }
 
+  TYPE_CLASS_MAP = {
+    mention:        'Mention',
+    reblog:         'Status',
+    follow:         'Follow',
+    follow_request: 'FollowRequest',
+    favourite:      'Favourite',
+  }.freeze
+
   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
 
@@ -28,12 +35,7 @@ class Notification < ApplicationRecord
   end
 
   def type
-    case activity_type
-    when 'Status'
-      :reblog
-    else
-      activity_type.underscore.to_sym
-    end
+    @type ||= TYPE_CLASS_MAP.invert[activity_type].to_sym
   end
 
   def target_status
@@ -50,6 +52,11 @@ class Notification < ApplicationRecord
   end
 
   class << self
+    def browserable(types = [])
+      types.concat([:follow_request])
+      where.not(activity_type: activity_types_from_types(types))
+    end
+
     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
@@ -58,6 +65,12 @@ class Notification < ApplicationRecord
         item.from_account = accounts[item.from_account_id]
       end
     end
+
+    private
+
+    def activity_types_from_types(types)
+      types.map { |type| TYPE_CLASS_MAP[type.to_sym] }.compact
+    end
   end
 
   after_initialize :set_from_account
diff --git a/app/models/status.rb b/app/models/status.rb
index 7e3dd3e28..16cd4383f 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -75,7 +75,7 @@ class Status < ApplicationRecord
   end
 
   def title
-    content
+    reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
   end
 
   def hidden?
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 15625ca43..6209d7dab 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -17,7 +17,7 @@ class Tag < ApplicationRecord
       textsearch = 'to_tsvector(\'simple\', tags.name)'
       query      = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
 
-      sql = <<SQL
+      sql = <<-SQL.squish
         SELECT
           tags.*,
           ts_rank_cd(#{textsearch}, #{query}) AS rank
@@ -25,7 +25,7 @@ class Tag < ApplicationRecord
         WHERE #{query} @@ #{textsearch}
         ORDER BY rank DESC
         LIMIT ?
-SQL
+      SQL
 
       Tag.find_by_sql([sql, limit])
     end
diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb
new file mode 100644
index 000000000..cd809566f
--- /dev/null
+++ b/app/presenters/instance_presenter.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class InstancePresenter
+  delegate(
+    :closed_registrations_message,
+    :contact_email,
+    :open_registrations,
+    :site_description,
+    :site_extended_description,
+    to: Setting
+  )
+
+  def contact_account
+    Account.find_local(Setting.site_contact_username)
+  end
+
+  def user_count
+    Rails.cache.fetch('user_count') { User.count }
+  end
+
+  def status_count
+    Rails.cache.fetch('local_status_count') { Status.local.count }
+  end
+
+  def domain_count
+    Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
+  end
+end
diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb
index 6a6a696d6..50ffc47c6 100644
--- a/app/services/fetch_remote_account_service.rb
+++ b/app/services/fetch_remote_account_service.rb
@@ -19,11 +19,16 @@ class FetchRemoteAccountService < BaseService
     xml = Nokogiri::XML(body)
     xml.encoding = 'utf-8'
 
-    url_parts = Addressable::URI.parse(url)
-    username  = xml.at_xpath('//xmlns:author/xmlns:name').try(:content)
-    domain    = url_parts.host
+    email = xml.at_xpath('//xmlns:author/xmlns:email').try(:content)
+    if email.nil?
+      url_parts = Addressable::URI.parse(url)
+      username  = xml.at_xpath('//xmlns:author/xmlns:name').try(:content)
+      domain    = url_parts.host
+    else
+      username, domain = email.split('@')
+    end
 
-    return nil if username.nil?
+    return nil if username.nil? || domain.nil?
 
     Rails.logger.debug "Going to webfinger #{username}@#{domain}"
 
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
new file mode 100644
index 000000000..c7a9a488b
--- /dev/null
+++ b/app/views/about/_registration.html.haml
@@ -0,0 +1,30 @@
+= simple_form_for(new_user, url: user_registration_path) do |f|
+  = f.simple_fields_for :account do |account_fields|
+    = account_fields.input :username,
+      autofocus: true,
+      placeholder: t('simple_form.labels.defaults.username'),
+      required: true,
+      input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
+
+  = f.input :email,
+    placeholder: t('simple_form.labels.defaults.email'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
+  = f.input :password,
+    autocomplete: "off",
+    placeholder: t('simple_form.labels.defaults.password'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
+  = f.input :password_confirmation,
+    autocomplete: "off",
+    placeholder: t('simple_form.labels.defaults.confirm_password'),
+    required: true,
+    input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
+
+  .actions
+    = f.button :button, t('about.get_started'), type: :submit
+
+  .info
+    = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
+    ·
+    = link_to t('about.about_this'), about_more_path
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 2de3bf986..8c12f57c1 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -7,42 +7,42 @@
       .panel
         %h2= Rails.configuration.x.local_domain
 
-        - unless @description.blank?
-          %p= @description.html_safe
+        - unless @instance_presenter.site_description.blank?
+          %p= @instance_presenter.site_description.html_safe
 
       .information-board
         .section
           %span= t 'about.user_count_before'
-          %strong= number_with_delimiter @user_count
+          %strong= number_with_delimiter @instance_presenter.user_count
           %span= t 'about.user_count_after'
         .section
           %span= t 'about.status_count_before'
-          %strong= number_with_delimiter @status_count
+          %strong= number_with_delimiter @instance_presenter.status_count
           %span= t 'about.status_count_after'
         .section
           %span= t 'about.domain_count_before'
-          %strong= number_with_delimiter @domain_count
+          %strong= number_with_delimiter @instance_presenter.domain_count
           %span= t 'about.domain_count_after'
 
-      - unless @extended_description.blank?
-        .panel= @extended_description.html_safe
+      - unless @instance_presenter.site_extended_description.blank?
+        .panel= @instance_presenter.site_extended_description.html_safe
 
     .sidebar
       .panel
         .panel-header= t 'about.contact'
         .panel-body
-          - if @contact_account
+          - if @instance_presenter.contact_account
             .owner
-              .avatar= image_tag @contact_account.avatar.url
+              .avatar= image_tag @instance_presenter.contact_account.avatar.url
               .name
-                = link_to TagManager.instance.url_for(@contact_account) do
-                  %span.display_name.emojify= display_name(@contact_account)
-                  %span.username= "@#{@contact_account.acct}"
+                = link_to TagManager.instance.url_for(@instance_presenter.contact_account) do
+                  %span.display_name.emojify= display_name(@instance_presenter.contact_account)
+                  %span.username= "@#{@instance_presenter.contact_account.acct}"
 
-          - unless @contact_email.blank?
+          - unless @instance_presenter.contact_email.blank?
             .contact-email
               = t 'about.business_email'
-              %strong= @contact_email
+              %strong= @instance_presenter.contact_email
       .panel
         .panel-header= t 'about.links'
         .panel-list
diff --git a/app/views/about/index.html.haml b/app/views/about/show.html.haml
index f6b0c1668..8a0d00daa 100644
--- a/app/views/about/index.html.haml
+++ b/app/views/about/show.html.haml
@@ -8,7 +8,7 @@
   %meta{ property: 'og:site_name', content: site_title }/
   %meta{ property: 'og:type', content: 'website' }/
   %meta{ property: 'og:title', content: Rails.configuration.x.local_domain }/
-  %meta{ property: 'og:description', content: @description.blank? ? "Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly" : strip_tags(@description) }/
+  %meta{ property: 'og:description', content: strip_tags(@instance_presenter.site_description.blank? ? t('about.about_mastodon') : @instance_presenter.site_description) }/
   %meta{ property: 'og:image', content: asset_url('mastodon_small.jpg') }/
   %meta{ property: 'og:image:width', content: '400' }/
   %meta{ property: 'og:image:height', content: '400' }/
@@ -24,28 +24,14 @@
   .screenshot-with-signup
     .mascot= image_tag 'fluffy-elephant-friend.png'
 
-    - if @open_registrations
-      = simple_form_for(@user, url: user_registration_path) do |f|
-        = f.simple_fields_for :account do |ff|
-          = ff.input :username, autofocus: true, placeholder: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username') }
-
-        = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
-        = f.input :password, autocomplete: "off", placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password') }
-        = f.input :password_confirmation, autocomplete: "off", placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password') }
-
-        .actions
-          = f.button :button, t('about.get_started'), type: :submit
-
-        .info
-          = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
-          ·
-          = link_to t('about.about_this'), about_more_path
+    - if @instance_presenter.open_registrations
+      = render 'registration'
     - else
       .closed-registrations-message
-        - if @closed_registrations_message.blank?
+        - if @instance_presenter.closed_registrations_message.blank?
           %p= t('about.closed_registrations')
         - else
-          = @closed_registrations_message.html_safe
+          = @instance_presenter.closed_registrations_message.html_safe
         .info
           = link_to t('auth.login'), new_user_session_path, class: 'webapp-btn'
           ·
@@ -85,9 +71,9 @@
           = fa_icon('li check-square')
           = t 'about.features.api'
 
-  - unless @description.blank?
+  - unless @instance_presenter.site_description.blank?
     %h3= t('about.description_headline', domain: Rails.configuration.x.local_domain)
-    %p= @description.html_safe
+    %p= @instance_presenter.site_description.html_safe
 
   .actions
     .info
diff --git a/app/views/accounts/followers.html.haml b/app/views/accounts/followers.html.haml
index 493491020..fa5071f38 100644
--- a/app/views/accounts/followers.html.haml
+++ b/app/views/accounts/followers.html.haml
@@ -9,4 +9,4 @@
   - else
     = render partial: 'grid_card', collection: @followers, as: :account, cached: true
 
-= will_paginate @followers, pagination_options
+= paginate @followers
diff --git a/app/views/accounts/following.html.haml b/app/views/accounts/following.html.haml
index 370cd6c48..987dcba1f 100644
--- a/app/views/accounts/following.html.haml
+++ b/app/views/accounts/following.html.haml
@@ -9,4 +9,4 @@
   - else
     = render partial: 'grid_card', collection: @following, as: :account, cached: true
 
-= will_paginate @following, pagination_options
+= paginate @following
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index e90897729..3b0d69dcd 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -31,4 +31,4 @@
 
   .pagination
     - if @statuses.size == 20
-      = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
+      = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), short_account_url(@account, max_id: @statuses.last.id), class: 'next', rel: 'next'
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index f8ed4ef97..4d636601e 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -46,4 +46,4 @@
           = table_link_to 'globe', 'Public', TagManager.instance.url_for(account)
           = table_link_to 'pencil', 'Edit', admin_account_path(account.id)
 
-= will_paginate @accounts, pagination_options
+= paginate @accounts
diff --git a/app/views/admin/domain_blocks/index.html.haml b/app/views/admin/domain_blocks/index.html.haml
index eb7894b86..fe6ff683f 100644
--- a/app/views/admin/domain_blocks/index.html.haml
+++ b/app/views/admin/domain_blocks/index.html.haml
@@ -13,5 +13,5 @@
           %samp= block.domain
         %td= block.severity
 
-= will_paginate @blocks, pagination_options
+= paginate @blocks
 = link_to 'Add new', new_admin_domain_block_path, class: 'button'
diff --git a/app/views/admin/pubsubhubbub/index.html.haml b/app/views/admin/pubsubhubbub/index.html.haml
index cb11a502c..2b8e36e6a 100644
--- a/app/views/admin/pubsubhubbub/index.html.haml
+++ b/app/views/admin/pubsubhubbub/index.html.haml
@@ -26,4 +26,4 @@
           - else
             = l subscription.last_successful_delivery_at
 
-= will_paginate @subscriptions, pagination_options
+= paginate @subscriptions
diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml
index 839259dc2..9c5c78935 100644
--- a/app/views/admin/reports/index.html.haml
+++ b/app/views/admin/reports/index.html.haml
@@ -29,4 +29,4 @@
           %td= truncate(report.comment, length: 30, separator: ' ')
           %td= table_link_to 'circle', 'View', admin_report_path(report)
 
-= will_paginate @reports, pagination_options
+= paginate @reports
diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl
index 32df0457a..8826aa22d 100644
--- a/app/views/api/v1/accounts/show.rabl
+++ b/app/views/api/v1/accounts/show.rabl
@@ -4,8 +4,9 @@ attributes :id, :username, :acct, :display_name, :locked, :created_at
 
 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(: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.followers_count }
-node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
-node(:statuses_count)  { |account| defined?(@statuses_counts_map)  ? (@statuses_counts_map[account.id]  || 0) : account.statuses_count }
+node(:avatar)          { |account| full_asset_url(account.avatar_original_url) }
+node(:avatar_static)   { |account| full_asset_url(account.avatar_static_url) }
+node(:header)          { |account| full_asset_url(account.header_original_url) }
+node(:header_static)   { |account| full_asset_url(account.header_static_url) }
+
+attributes :followers_count, :following_count, :statuses_count
diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml
new file mode 100644
index 000000000..30a3643d6
--- /dev/null
+++ b/app/views/kaminari/_next_page.html.haml
@@ -0,0 +1,9 @@
+-#  Link to the "Next" page
+-#  available local variables
+-#    url:           url to the next page
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+%span.next
+  = link_to_unless current_page.last?, safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), url, rel: 'next', remote: remote
diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml
new file mode 100644
index 000000000..b1da236d5
--- /dev/null
+++ b/app/views/kaminari/_paginator.html.haml
@@ -0,0 +1,16 @@
+-#  The container tag
+-#  available local variables
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+-#    paginator:     the paginator that renders the pagination tags inside
+= paginator.render do
+  %nav.pagination
+    = prev_page_tag unless current_page.first?
+    - each_page do |page|
+      - if page.display_tag?
+        = page_tag page
+      - elsif !page.was_truncated?
+        = gap_tag
+    = next_page_tag unless current_page.last?
diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml
new file mode 100644
index 000000000..1089e3566
--- /dev/null
+++ b/app/views/kaminari/_prev_page.html.haml
@@ -0,0 +1,9 @@
+-#  Link to the "Previous" page
+-#  available local variables
+-#    url:           url to the previous page
+-#    current_page:  a page object for the currently displayed page
+-#    total_pages:   total number of pages
+-#    per_page:      number of items to fetch per page
+-#    remote:        data-remote
+%span.prev
+  = link_to_unless current_page.first?, safe_join([fa_icon('chevron-left'), t('pagination.prev')], ' '), url, rel: 'prev', remote: remote
diff --git a/app/views/shared/_landing_strip.html.haml b/app/views/shared/_landing_strip.html.haml
index bb081e544..3536c5ca8 100644
--- a/app/views/shared/_landing_strip.html.haml
+++ b/app/views/shared/_landing_strip.html.haml
@@ -1,2 +1,5 @@
 .landing-strip
-  = t('landing_strip_html', name: display_name(account), domain: Rails.configuration.x.local_domain, sign_up_path: new_user_registration_path)
+  = t('landing_strip_html',
+    name: content_tag(:span, display_name(account), class: :emojify),
+    domain: Rails.configuration.x.local_domain,
+    sign_up_path: new_user_registration_path)
diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml
index 434c5c8da..1333d4d82 100644
--- a/app/views/stream_entries/_status.html.haml
+++ b/app/views/stream_entries/_status.html.haml
@@ -13,7 +13,7 @@
         = fa_icon('retweet fw')
       %span
         = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do
-          %strong= display_name(status.account)
+          %strong.emojify= display_name(status.account)
         = t('stream_entries.reblogged')
 
   = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: status.proper }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 32a50e158..c894cdb2e 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -15,4 +15,4 @@
 
 - if @statuses.size == 20
   .pagination
-    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next_page', rel: 'next'
+    = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), tag_url(@tag, max_id: @statuses.last.id), class: 'next', rel: 'next'
diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb
index 2665f1a20..6c45f1a21 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.html.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb
@@ -1,5 +1,5 @@
 <p>Bienvenue <%= @resource.email %>&nbsp;!</p>
 
-<p>Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous&nbsp;:</p>
+<p>Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous&nbsp;:</p>
 
 <p><%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb
index 9d33450f8..dfa3f9f7c 100644
--- a/app/views/user_mailer/confirmation_instructions.fr.text.erb
+++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb
@@ -1,5 +1,5 @@
 Bienvenue <%= @resource.email %> !
 
-Vous pouvez confirmer l'email de votre compte Mastodon en cliquant sur le lien ci-dessous :
+Vous pouvez confirmer le courriel de votre compte Mastodon en cliquant sur le lien ci-dessous :
 
 <%= confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
index d5a33cada..ad4f1b004 100644
--- a/app/workers/import_worker.rb
+++ b/app/workers/import_worker.rb
@@ -25,7 +25,7 @@ class ImportWorker
   def process_blocks(import)
     from_account = import.account
 
-    CSV.foreach(import.data.path) do |row|
+    CSV.new(open(import.data.url)).each do |row|
       next if row.size != 1
 
       begin
@@ -41,7 +41,7 @@ class ImportWorker
   def process_follows(import)
     from_account = import.account
 
-    CSV.foreach(import.data.path) do |row|
+    CSV.new(open(import.data.url)).each do |row|
       next if row.size != 1
 
       begin