about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-10-16 01:29:02 -0500
committerDavid Yip <yipdw@member.fsf.org>2017-10-16 01:29:02 -0500
commit6cd5b3bbe5a11fcf25bbefba2803f2ae840f39fc (patch)
tree4bb60f4493fb70cada728a373f74c18b87e8f95d /app
parentf72ad67a3967230afd63a9e2d84a2a69331c4787 (diff)
parent894da3dcca781e27ce9c5130f1021526ac8a6887 (diff)
Merge remote-tracking branch 'origin/master' into gs-master
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/custom_emojis_controller.rb6
-rw-r--r--app/controllers/auth/registrations_controller.rb2
-rw-r--r--app/controllers/concerns/user_tracking_concern.rb6
-rw-r--r--app/javascript/mastodon/actions/accounts.js5
-rw-r--r--app/javascript/mastodon/components/relative_timestamp.js4
-rw-r--r--app/javascript/mastodon/features/compose/util/counter.js2
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js14
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json103
-rw-r--r--app/javascript/mastodon/locales/de.json4
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json2
-rw-r--r--app/javascript/mastodon/locales/en.json2
-rw-r--r--app/javascript/mastodon/locales/eo.json2
-rw-r--r--app/javascript/mastodon/locales/fa.json20
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json14
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json4
-rw-r--r--app/javascript/mastodon/locales/oc.json4
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json28
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/reducers/timelines.js9
-rw-r--r--app/javascript/styles/about.scss130
-rw-r--r--app/lib/activitypub/activity/undo.rb3
-rw-r--r--app/lib/feed_manager.rb5
-rw-r--r--app/lib/ostatus/atom_serializer.rb16
-rw-r--r--app/models/notification.rb5
-rw-r--r--app/models/status.rb12
-rw-r--r--app/serializers/activitypub/activity_serializer.rb5
-rw-r--r--app/services/fetch_link_card_service.rb4
-rw-r--r--app/services/fetch_remote_status_service.rb2
-rw-r--r--app/services/send_interaction_service.rb4
-rw-r--r--app/services/subscribe_service.rb3
-rw-r--r--app/services/unsubscribe_service.rb3
-rw-r--r--app/views/admin/custom_emojis/_custom_emoji.html.haml2
-rw-r--r--app/views/tags/show.html.haml28
-rw-r--r--app/workers/activitypub/delivery_worker.rb3
-rw-r--r--app/workers/pubsubhubbub/delivery_worker.rb3
38 files changed, 261 insertions, 206 deletions
diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb
index ca81f3255..5cce5bce4 100644
--- a/app/controllers/admin/custom_emojis_controller.rb
+++ b/app/controllers/admin/custom_emojis_controller.rb
@@ -31,10 +31,12 @@ module Admin
       emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
 
       if emoji.save
-        redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.copied_msg')
+        flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
       else
-        redirect_to admin_custom_emojis_path, alert: I18n.t('admin.custom_emojis.copy_failed_msg')
+        flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
       end
+
+      redirect_to admin_custom_emojis_path(params[:page])
     end
 
     def enable
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index aac3c31ff..223db96ff 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -6,7 +6,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :check_enabled_registrations, only: [:new, :create]
   before_action :configure_sign_up_params, only: [:create]
   before_action :set_sessions, only: [:edit, :update]
-  before_action :set_instance_presenter, only: [:new, :update]
+  before_action :set_instance_presenter, only: [:new, :create, :update]
 
   def destroy
     not_found
diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb
index 8a63af95d..8663c3086 100644
--- a/app/controllers/concerns/user_tracking_concern.rb
+++ b/app/controllers/concerns/user_tracking_concern.rb
@@ -7,12 +7,14 @@ module UserTrackingConcern
   UPDATE_SIGN_IN_HOURS = 24
 
   included do
-    before_action :set_user_activity, if: %i(user_signed_in? user_needs_sign_in_update?)
+    before_action :set_user_activity
   end
 
   private
 
   def set_user_activity
+    return unless user_needs_sign_in_update?
+
     # Mark as signed-in today
     current_user.update_tracked_fields!(request)
 
@@ -21,7 +23,7 @@ module UserTrackingConcern
   end
 
   def user_needs_sign_in_update?
-    current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago
+    user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
   end
 
   def user_needs_feed_update?
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index fc47110e3..fbaebf786 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -122,7 +122,7 @@ export function unfollowAccount(id) {
     dispatch(unfollowAccountRequest(id));
 
     api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => {
-      dispatch(unfollowAccountSuccess(response.data));
+      dispatch(unfollowAccountSuccess(response.data, getState().get('statuses')));
     }).catch(error => {
       dispatch(unfollowAccountFail(error));
     });
@@ -157,10 +157,11 @@ export function unfollowAccountRequest(id) {
   };
 };
 
-export function unfollowAccountSuccess(relationship) {
+export function unfollowAccountSuccess(relationship, statuses) {
   return {
     type: ACCOUNT_UNFOLLOW_SUCCESS,
     relationship,
+    statuses,
   };
 };
 
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 534d83fac..51588e78c 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -94,6 +94,10 @@ export default class RelativeTimestamp extends React.Component {
     this._scheduleNextUpdate(nextProps, nextState);
   }
 
+  componentWillUnmount () {
+    clearTimeout(this._timer);
+  }
+
   _scheduleNextUpdate (props, state) {
     clearTimeout(this._timer);
 
diff --git a/app/javascript/mastodon/features/compose/util/counter.js b/app/javascript/mastodon/features/compose/util/counter.js
index 588a372c6..e6d2487c5 100644
--- a/app/javascript/mastodon/features/compose/util/counter.js
+++ b/app/javascript/mastodon/features/compose/util/counter.js
@@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
 export function countableText(inputText) {
   return inputText
     .replace(urlRegex, urlPlaceholder)
-    .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
+    .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2');
 };
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 8d74783a3..b35347ba6 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -16,7 +16,6 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
-  hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
 });
 
 @connect(mapStateToProps)
@@ -27,7 +26,6 @@ export default class HomeTimeline extends React.PureComponent {
     dispatch: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     hasUnread: PropTypes.bool,
-    hasFollows: PropTypes.bool,
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
   };
@@ -60,17 +58,9 @@ export default class HomeTimeline extends React.PureComponent {
   }
 
   render () {
-    const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props;
+    const { intl, hasUnread, columnId, multiColumn } = this.props;
     const pinned = !!columnId;
 
-    let emptyMessage;
-
-    if (hasFollows) {
-      emptyMessage = <FormattedMessage id='empty_column.home.inactivity' defaultMessage='Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.' />;
-    } else {
-      emptyMessage = <FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />;
-    }
-
     return (
       <Column ref={this.setRef} name='home'>
         <ColumnHeader
@@ -91,7 +81,7 @@ export default class HomeTimeline extends React.PureComponent {
           scrollKey={`home_timeline-${columnId}`}
           loadMore={this.handleLoadMore}
           timelineId='home'
-          emptyMessage={emptyMessage}
+          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
         />
       </Column>
     );
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 240e3725e..eeded31b7 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index b5051a32d..fe2433591 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Bloquejar @{name}",
   "account.block_domain": "Amagar tot de {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "La informació següent pot reflectir incompleta el perfil de l'usuari.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidors",
@@ -13,19 +13,19 @@
   "account.posts": "Publicacions",
   "account.report": "Informe @{name}",
   "account.requested": "Esperant aprovació",
-  "account.share": "Share @{name}'s profile",
+  "account.share": "Compartir el perfil de @{name}",
   "account.unblock": "Desbloquejar @{name}",
   "account.unblock_domain": "Mostra {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Treure silenci de @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.view_full_profile": "Veure el perfil complet",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
+  "bundle_column_error.retry": "Torna-ho a provar",
+  "bundle_column_error.title": "Error de connexió",
+  "bundle_modal_error.close": "Tanca",
+  "bundle_modal_error.message": "S'ha produït un error en carregar aquest component.",
+  "bundle_modal_error.retry": "Torna-ho a provar",
   "column.blocks": "Usuaris bloquejats",
   "column.community": "Línia de temps local",
   "column.favourites": "Favorits",
@@ -33,15 +33,15 @@
   "column.home": "Inici",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
-  "column.pins": "Pinned toot",
+  "column.pins": "Toot fixat",
   "column.public": "Línia de temps federada",
   "column_back_button.label": "Enrere",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "Amaga la configuració",
+  "column_header.moveLeft_settings": "Mou la columna cap a l'esquerra",
+  "column_header.moveRight_settings": "Mou la columna cap a la dreta",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostra la configuració",
+  "column_header.unpin": "Deslligar",
   "column_subheading.navigation": "Navegació",
   "column_subheading.settings": "Configuració",
   "compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
@@ -61,22 +61,22 @@
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.mute.message": "Estàs segur que vols silenciar {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "Estàs segur que vols deixar de seguir {name}?",
+  "embed.instructions": "Incrusta aquest estat al lloc web copiant el codi a continuació.",
+  "embed.preview": "A continuació s'explica com:",
   "emoji_button.activity": "Activitat",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "Personalitzat",
   "emoji_button.flags": "Flags",
   "emoji_button.food": "Menjar i Beure",
   "emoji_button.label": "Inserir emoji",
   "emoji_button.nature": "Natura",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Emojos no!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objectes",
   "emoji_button.people": "Gent",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Freqüentment utilitzat",
   "emoji_button.search": "Cercar...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Resultats de la cerca",
   "emoji_button.symbols": "Símbols",
   "emoji_button.travel": "Viatges i Llocs",
   "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per fer rodar la pilota!",
@@ -88,7 +88,7 @@
   "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
   "follow_request.authorize": "Autoritzar",
   "follow_request.reject": "Rebutjar",
-  "getting_started.appsshort": "Apps",
+  "getting_started.appsshort": "Aplicacions",
   "getting_started.faq": "PMF",
   "getting_started.heading": "Començant",
   "getting_started.open_source_notice": "Mastodon és un programari de codi obert. Pots contribuir o informar de problemes a GitHub de {github}.",
@@ -100,8 +100,8 @@
   "home.column_settings.show_replies": "Mostrar respostes",
   "home.settings": "Ajustos de columna",
   "lightbox.close": "Tancar",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "Següent",
+  "lightbox.previous": "Anterior",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -113,7 +113,7 @@
   "navigation_bar.info": "Informació addicional",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "Toots fixats",
   "navigation_bar.preferences": "Preferències",
   "navigation_bar.public_timeline": "Línia de temps federada",
   "notification.favourite": "{name} ha afavorit el teu estat",
@@ -126,8 +126,8 @@
   "notifications.column_settings.favourite": "Favorits:",
   "notifications.column_settings.follow": "Nous seguidors:",
   "notifications.column_settings.mention": "Mencions:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "Push notificacions",
+  "notifications.column_settings.push_meta": "Aquest dispositiu",
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
@@ -160,28 +160,33 @@
   "privacy.public.short": "Públic",
   "privacy.unlisted.long": "No publicar en línies de temps públiques",
   "privacy.unlisted.short": "No llistat",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "now",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancel·lar",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
   "report.target": "Informes",
   "search.placeholder": "Cercar",
-  "search_popout.search_format": "Advanced search format",
+  "search_popout.search_format": "Format de cerca avançada",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
-  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
-  "standalone.public_title": "A look inside...",
+  "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
+  "search_popout.tips.user": "usuari",
+  "search_results.total": "{count, number} {count, plural, un {result} altres {results}}",
+  "standalone.public_title": "Una mirada a l'interior ...",
   "status.cannot_reblog": "Aquesta publicació no pot ser retootejada",
   "status.delete": "Esborrar",
-  "status.embed": "Embed",
+  "status.embed": "Incrustar",
   "status.favourite": "Favorit",
   "status.load_more": "Carrega més",
   "status.media_hidden": "Multimèdia amagat",
   "status.mention": "Esmentar @{name}",
   "status.mute_conversation": "Silenciar conversació",
   "status.open": "Ampliar aquest estat",
-  "status.pin": "Pin on profile",
+  "status.pin": "Fixat en el perfil",
   "status.reblog": "Boost",
   "status.reblogged_by": "{name} ha retootejat",
   "status.reply": "Respondre",
@@ -189,11 +194,11 @@
   "status.report": "Informar sobre @{name}",
   "status.sensitive_toggle": "Clic per veure",
   "status.sensitive_warning": "Contingut sensible",
-  "status.share": "Share",
+  "status.share": "Compartir",
   "status.show_less": "Mostra menys",
   "status.show_more": "Mostra més",
   "status.unmute_conversation": "Activar conversació",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "Deslliga del perfil",
   "tabs_bar.compose": "Compondre",
   "tabs_bar.federated_timeline": "Federada",
   "tabs_bar.home": "Inici",
@@ -201,16 +206,16 @@
   "tabs_bar.notifications": "Notificacions",
   "upload_area.title": "Arrossega i deixa anar per carregar",
   "upload_button.label": "Afegir multimèdia",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "Descriure els problemes visuals",
   "upload_form.undo": "Desfer",
   "upload_progress.label": "Pujant...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
-  "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
-  "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.close": "Tancar el vídeo",
+  "video.exit_fullscreen": "Surt de pantalla completa",
+  "video.expand": "Ampliar el vídeo",
+  "video.fullscreen": "Pantalla completa",
+  "video.hide": "Amaga vídeo",
+  "video.mute": "Silenciar el so",
+  "video.pause": "Pausa",
+  "video.play": "Reproduir",
+  "video.unmute": "Activar so"
 }
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index b79b1b2f0..9d9853236 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Reisen und Orte",
   "empty_column.community": "Die lokale Zeitleiste ist leer. Schreibe einen öffentlichen Beitrag, um den Ball ins Rollen zu bringen!",
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
-  "empty_column.home": "Du folgst noch niemandem. Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
+  "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
   "empty_column.home.inactivity": "Deine Zeitleiste ist leer. Falls du eine längere Zeit inaktiv warst, wird sie für dich so schnell wie möglich neu erstellt.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
@@ -143,7 +143,7 @@
   "onboarding.page_six.almost_done": "Fast fertig …",
   "onboarding.page_six.appetoot": "Guten Appetröt!",
   "onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und weitere Plattformen.",
-  "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf GitHub unter {github} dazu beitragen, Probleme melden und Wünsche äußern.",
+  "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen, Probleme melden und Wünsche äußern.",
   "onboarding.page_six.guidelines": "Richtlinien",
   "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut.",
   "onboarding.page_six.various_app": "Apps",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 8fdb8c44c..99ff3b35b 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -912,7 +912,7 @@
         "id": "empty_column.home.inactivity"
       },
       {
-        "defaultMessage": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+        "defaultMessage": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
         "id": "empty_column.home"
       },
       {
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index b0dbc46bd..12efe0e0c 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 1ccd2b817..8f90bdf78 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 13fb91278..9df0dec42 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -66,17 +66,17 @@
   "embed.instructions": "برای جاگذاری این نوشته در سایت خودتان، کد زیر را کپی کنید.",
   "embed.preview": "نوشتهٔ جاگذاری‌شده این گونه به نظر خواهد رسید:",
   "emoji_button.activity": "فعالیت",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "سفارشی",
   "emoji_button.flags": "پرچم‌ها",
   "emoji_button.food": "غذا و نوشیدنی",
   "emoji_button.label": "افزودن شکلک",
   "emoji_button.nature": "طبیعت",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "این‌جا شکلکی نیست!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "اشیا",
   "emoji_button.people": "مردم",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "زیاد به‌کاررفته",
   "emoji_button.search": "جستجو...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "نتایج جستجو",
   "emoji_button.symbols": "نمادها",
   "emoji_button.travel": "سفر و مکان",
   "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
@@ -165,11 +165,11 @@
   "report.submit": "بفرست",
   "report.target": "گزارش‌دادن",
   "search.placeholder": "جستجو",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.search_format": "راهنمای جستجوی پیشرفته",
+  "search_popout.tips.hashtag": "هشتگ",
+  "search_popout.tips.status": "نوشته",
+  "search_popout.tips.text": "جستجوی متنی ساده برای نام‌ها، نام‌های کاربری، و هشتگ‌ها",
+  "search_popout.tips.user": "کاربر",
   "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
   "standalone.public_title": "نگاهی به کاربران این سرور...",
   "status.cannot_reblog": "این نوشته را نمی‌شود بازبوقید",
@@ -201,7 +201,7 @@
   "tabs_bar.notifications": "اعلان‌ها",
   "upload_area.title": "برای بارگذاری به این‌جا بکشید",
   "upload_button.label": "افزودن تصویر",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "نوشتهٔ توضیحی برای کم‌بینایان و نابینایان",
   "upload_form.undo": "واگردانی",
   "upload_progress.label": "بارگذاری...",
   "video.close": "بستن ویدیو",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 425b3d82a..0f6554595 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 4eca47b60..145b683f3 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -54,13 +54,13 @@
   "compose_form.spoiler_placeholder": "Écrivez ici votre avertissement",
   "confirmation_modal.cancel": "Annuler",
   "confirmations.block.confirm": "Bloquer",
-  "confirmations.block.message": "Confirmez vous le blocage de {name} ?",
+  "confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
   "confirmations.delete.confirm": "Supprimer",
-  "confirmations.delete.message": "Confirmez vous la suppression de ce pouet ?",
+  "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
   "confirmations.domain_block.confirm": "Masquer le domaine entier",
   "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Masquer",
-  "confirmations.mute.message": "Confirmez vous le masquage de {name} ?",
+  "confirmations.mute.message": "Confirmez-vous le masquage de {name} ?",
   "confirmations.unfollow.confirm": "Ne plus suivre",
   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
   "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
@@ -69,7 +69,7 @@
   "emoji_button.custom": "Personnalisés",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
-  "emoji_button.label": "Insérer un emoji",
+  "emoji_button.label": "Insérer un émoji",
   "emoji_button.nature": "Nature",
   "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objets",
@@ -79,7 +79,7 @@
   "emoji_button.search_results": "Résultats de la recherche",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux et voyages",
-  "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
+  "empty_column.community": "Le fil public local est vide. Écrivez donc quelque chose pour le remplir !",
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
@@ -124,7 +124,7 @@
   "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
   "notifications.column_settings.favourite": "Favoris :",
-  "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
+  "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e⋅s :",
   "notifications.column_settings.mention": "Mentions :",
   "notifications.column_settings.push": "Notifications push",
   "notifications.column_settings.push_meta": "Cet appareil",
@@ -139,7 +139,7 @@
   "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
   "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}",
   "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
-  "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
+  "onboarding.page_six.admin": "L’administrateur⋅ice de votre instance est {admin}",
   "onboarding.page_six.almost_done": "Nous y sommes presque…",
   "onboarding.page_six.appetoot": "Bon appouétit !",
   "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon appouétit !",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 7b9c1b293..2296ea71e 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index a3e237f6b..04b88da34 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -151,8 +151,8 @@
   "onboarding.page_three.search": "Gebruik de zoekbalk linksboven om andere mensen op Mastodon te vinden en om te zoeken op hashtags, zoals {illustration} en {introductions}. Om iemand te vinden die niet op deze Mastodon-server zit, moet je het volledige Mastodon-adres van deze persoon invoeren.",
   "onboarding.page_two.compose": "Schrijf berichten (wij noemen dit toots) in het tekstvak in de linkerkolom. Je kan met de pictogrammen daaronder afbeeldingen uploaden, privacy-instellingen veranderen en je tekst een waarschuwing meegeven.",
   "onboarding.skip": "Overslaan",
-  "privacy.change": "Privacy toot aanpassen",
-  "privacy.direct.long": "Toot alleen naar vermelde gebruikers",
+  "privacy.change": "Zichtbaarheid toot aanpassen",
+  "privacy.direct.long": "Alleen aan vermelde gebruikers tonen",
   "privacy.direct.short": "Direct",
   "privacy.private.long": "Alleen aan volgers tonen",
   "privacy.private.short": "Alleen volgers",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index d730b47f4..4715f60ef 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Viatges & lòcs",
   "empty_column.community": "Lo flux public local es void. Escrivètz quicòm per lo garnir !",
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
-  "empty_column.home": "Pel moment seguètz pas degun. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
+  "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.inactivity": "Vòstra pagina d’acuèlh es voida. Se sètz estat inactiu per un moment, serà tornada generar per vos dins una estona.",
   "empty_column.home.public_timeline": "lo flux public",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
@@ -201,7 +201,7 @@
   "tabs_bar.notifications": "Notificacions",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
-  "upload_form.description": "Descripcion se per cas i aja un problèma",
+  "upload_form.description": "Descripcion pels mal vesents",
   "upload_form.undo": "Anullar",
   "upload_progress.label": "Mandadís…",
   "video.close": "Tampar la vidèo",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 61674b37e..2c79a7509 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -45,7 +45,7 @@
   "column_subheading.navigation": "Navegação",
   "column_subheading.settings": "Configurações",
   "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
-  "compose_form.lock_disclaimer.lock": "trancado",
+  "compose_form.lock_disclaimer.lock": "trancada",
   "compose_form.placeholder": "No que você está pensando?",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
@@ -66,17 +66,17 @@
   "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
   "embed.preview": "Aqui está uma previsão de como ficará:",
   "emoji_button.activity": "Atividades",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "Customizados",
   "emoji_button.flags": "Bandeiras",
   "emoji_button.food": "Comidas & Bebidas",
   "emoji_button.label": "Inserir Emoji",
   "emoji_button.nature": "Natureza",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Não tem emojos! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Pessoas",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Usados frequentemente",
   "emoji_button.search": "Buscar...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Resultados da busca",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
@@ -148,7 +148,7 @@
   "onboarding.page_six.read_guidelines": "Por favor, leia as {guidelines} do {domain}!",
   "onboarding.page_six.various_app": "aplicativos móveis",
   "onboarding.page_three.profile": "Edite o seu perfil para mudar o seu o seu avatar, bio e nome de exibição. No menu de configurações, você também encontrará outras preferências.",
-  "onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtahs, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.",
+  "onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtags, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.",
   "onboarding.page_two.compose": "Escreva postagens na coluna de escrita. Você pode hospedar imagens, mudar as configurações de privacidade e adicionar alertas de conteúdo através dos ícones abaixo.",
   "onboarding.skip": "Pular",
   "privacy.change": "Ajustar a privacidade da mensagem",
@@ -165,15 +165,15 @@
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
-  "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
+"search_popout.search_format": "Advanced search format",
+"search_popout.tips.hashtag": "hashtag",
+"search_popout.tips.status": "status",
+  "search_popout.tips.text": "Texto simples retorna nomes de exibição, usuários e hashtags correspondentes",
   "search_popout.tips.user": "user",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Dê uma espiada...",
   "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
-  "status.delete": "Eliminar",
+  "status.delete": "Excluir",
   "status.embed": "Incorporar",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
@@ -201,15 +201,15 @@
   "tabs_bar.notifications": "Notificações",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
-  "upload_form.description": "Describe for the visually impaired",
-  "upload_form.undo": "Anular",
+  "upload_form.description": "Descreva a imagem para deficientes visuais",
+  "upload_form.undo": "Desfazer",
   "upload_progress.label": "Salvando...",
   "video.close": "Fechar vídeo",
   "video.exit_fullscreen": "Sair da tela cheia",
   "video.expand": "Expandir vídeo",
   "video.fullscreen": "Tela cheia",
   "video.hide": "Esconder vídeo",
-  "video.mute": "Silenciar vídeo",
+  "video.mute": "Silenciar",
   "video.pause": "Parar",
   "video.play": "Reproduzir",
   "video.unmute": "Retirar silêncio"
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index f3ec9c532..4339d1497 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -81,7 +81,7 @@
   "emoji_button.travel": "Travel & Places",
   "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
   "empty_column.hashtag": "There is nothing in this hashtag yet.",
-  "empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.",
+  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
   "empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.",
   "empty_column.home.public_timeline": "the public timeline",
   "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 065e89f96..b17d74ef3 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -14,6 +14,7 @@ import {
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
+  ACCOUNT_UNFOLLOW_SUCCESS,
 } from '../actions/accounts';
 import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
@@ -108,6 +109,12 @@ const filterTimelines = (state, relationship, statuses) => {
   return state;
 };
 
+const filterTimeline = (timeline, state, relationship, statuses) =>
+  state.updateIn([timeline, 'items'], ImmutableList(), list =>
+    list.filterNot(statusId =>
+      statuses.getIn([statusId, 'account']) === relationship.id
+    ));
+
 const updateTop = (state, timeline, top) => {
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     if (top) mMap.set('unread', 0);
@@ -134,6 +141,8 @@ export default function timelines(state = initialState, action) {
   case ACCOUNT_BLOCK_SUCCESS:
   case ACCOUNT_MUTE_SUCCESS:
     return filterTimelines(state, action.relationship, action.statuses);
+  case ACCOUNT_UNFOLLOW_SUCCESS:
+    return filterTimeline('home', state, action.relationship, action.statuses);
   case TIMELINE_SCROLL_TOP:
     return updateTop(state, action.timeline, action.top);
   case TIMELINE_CONNECT:
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 8c37310e8..4ec689427 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -702,7 +702,6 @@
     .features #mastodon-timeline {
       height: 70vh;
       width: 100%;
-      min-width: 330px;
       margin-bottom: 50px;
 
       .column {
@@ -716,85 +715,96 @@
   }
 
   &.tag-page {
-    .brand {
-      padding-top: 20px;
-      margin-bottom: 20px;
+    .features {
+      padding: 30px 0;
 
-      img {
-        height: 48px;
-        width: auto;
-      }
-    }
+      .container {
+        max-width: 820px;
 
-    .container {
-      max-width: 690px;
-    }
+        #mastodon-timeline {
+          margin-right: 0;
+          border-top-right-radius: 0;
+        }
 
-    .cta {
-      margin: 40px 0;
-      margin-bottom: 80px;
+        .about-mastodon {
+          .about-hashtag {
+            background: darken($ui-base-color, 4%);
+            padding: 0 20px 20px 30px;
+            border-radius: 0 5px 5px 0;
 
-      .button {
-        margin-right: 4px;
-      }
-    }
+            .brand {
+              padding-top: 20px;
+              margin-bottom: 20px;
 
-    .about-mastodon {
-      max-width: 330px;
+              img {
+                height: 48px;
+                width: auto;
+              }
+            }
 
-      p {
-        strong {
-          color: $ui-secondary-color;
-          font-weight: 700;
+            p {
+              strong {
+                color: $ui-secondary-color;
+                font-weight: 700;
+              }
+            }
+
+            .cta {
+              margin: 0;
+
+              .button {
+                margin-right: 4px;
+              }
+            }
+          }
+
+          .features-list {
+            margin-left: 30px;
+            margin-right: 10px;
+          }
         }
       }
     }
 
     @media screen and (max-width: 675px) {
-      .container {
-        display: flex;
-        flex-direction: column;
-      }
-
       .features {
-        padding: 20px 0;
-      }
+        padding: 10px 0;
 
-      .about-mastodon {
-        order: 1;
-        flex: 0 0 auto;
-        max-width: 100%;
-      }
+        .container {
+          display: flex;
+          flex-direction: column;
 
-      #mastodon-timeline {
-        order: 2;
-        flex: 0 0 auto;
-        height: 60vh;
-      }
+          #mastodon-timeline {
+            order: 2;
+            flex: 0 0 auto;
+            height: 60vh;
+            margin-bottom: 20px;
+            border-top-right-radius: 4px;
+          }
 
-      .cta {
-        margin: 20px 0;
-        margin-bottom: 30px;
-      }
+          .about-mastodon {
+            order: 1;
+            flex: 0 0 auto;
+            max-width: 100%;
 
-      .features-list {
-        display: none;
-      }
+            .about-hashtag {
+              background: unset;
+              padding: 0;
+              border-radius: 0;
+
+              .cta {
+                margin: 20px 0;
+              }
+            }
 
-      .stripe {
-        display: none;
+            .features-list {
+              display: none;
+            }
+          }
+        }
       }
     }
   }
-
-  .stripe {
-    width: 100%;
-    height: 360px;
-    overflow: hidden;
-    background: darken($ui-base-color, 4%);
-    position: absolute;
-    z-index: -1;
-  }
 }
 
 @keyframes floating {
diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb
index 4b0905de2..cbed417c4 100644
--- a/app/lib/activitypub/activity/undo.rb
+++ b/app/lib/activitypub/activity/undo.rb
@@ -17,7 +17,8 @@ class ActivityPub::Activity::Undo < ActivityPub::Activity
   private
 
   def undo_announce
-    status = Status.find_by(uri: object_uri, account: @account)
+    status   = Status.find_by(uri: object_uri, account: @account)
+    status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present?
 
     if status.nil?
       delete_later!(object_uri)
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index f6a694135..5a3af7206 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -85,7 +85,7 @@ class FeedManager
     oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 
     from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status|
-      unpush(:home, into_account, status)
+      remove_from_feed(:home, into_account, status)
     end
   end
 
@@ -121,7 +121,8 @@ class FeedManager
     #return true if reggie === status.content || reggie === status.spoiler_text
     # extremely violent filtering code END
 
-    return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
+    return false if receiver_id == status.account_id
+    return true  if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
 
     check_for_mutes = [status.account_id]
     check_for_mutes.concat([status.reblog.account_id]) if status.reblog?
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index a1ac11a51..3ca6c5943 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -23,7 +23,7 @@ class OStatus::AtomSerializer
     append_element(author, 'name', account.username)
     append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct)
     append_element(author, 'summary', Formatter.instance.simplified_format(account).to_str, type: :html) if account.note?
-    append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
+    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))) if account.avatar?
     append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header?
     append_element(author, 'poco:preferredUsername', account.username)
@@ -47,7 +47,7 @@ class OStatus::AtomSerializer
 
     feed << author(account)
 
-    append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(account))
+    append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
     append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
     append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
     append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
@@ -86,9 +86,9 @@ class OStatus::AtomSerializer
       serialize_status_attributes(entry, stream_entry.status)
     end
 
-    append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(stream_entry.status))
+    append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(stream_entry.status))
     append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom'))
-    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
+    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: ::TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded?
     append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil?
 
     entry
@@ -109,8 +109,8 @@ class OStatus::AtomSerializer
 
     serialize_status_attributes(object, status)
 
-    append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: TagManager.instance.url_for(status))
-    append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: TagManager.instance.url_for(status.thread)) unless status.thread.nil?
+    append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(status))
+    append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: ::TagManager.instance.url_for(status.thread)) unless status.thread.nil?
     append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil?
 
     object
@@ -290,7 +290,7 @@ class OStatus::AtomSerializer
 
     entry << object(favourite.status)
 
-    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
+    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
 
     entry
   end
@@ -312,7 +312,7 @@ class OStatus::AtomSerializer
 
     entry << object(favourite.status)
 
-    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: TagManager.instance.url_for(favourite.status))
+    append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status))
 
     entry
   end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 1e64d1ae9..0a5d987cf 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -68,7 +68,10 @@ class Notification < ApplicationRecord
   class << self
     def reload_stale_associations!(cached_items)
       account_ids = cached_items.map(&:from_account_id).uniq
-      accounts    = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
+
+      return if account_ids.empty?
+
+      accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
 
       cached_items.each do |item|
         item.from_account = accounts[item.from_account_id]
diff --git a/app/models/status.rb b/app/models/status.rb
index 107ccface..30d53f298 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -134,7 +134,7 @@ class Status < ApplicationRecord
     CustomEmoji.from_text([spoiler_text, text].join(' '), account.domain)
   end
 
-  after_create :store_uri, if: :local?
+  after_create_commit :store_uri, if: :local?
 
   around_create Mastodon::Snowflake::Callbacks
 
@@ -194,7 +194,11 @@ class Status < ApplicationRecord
         account_ids << item.reblog.account_id if item.reblog?
       end
 
-      accounts = Account.where(id: account_ids.uniq).map { |a| [a.id, a] }.to_h
+      account_ids.uniq!
+
+      return if account_ids.empty?
+
+      accounts = Account.where(id: account_ids).map { |a| [a.id, a] }.to_h
 
       cached_items.each do |item|
         item.account = accounts[item.account_id]
@@ -216,9 +220,7 @@ class Status < ApplicationRecord
         # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
         visibility.push(:private) if account.following?(target_account)
 
-        joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
-          .where(arel_table[:visibility].in(visibility).or(Mention.arel_table[:id].not_eq(nil)))
-          .order(visibility: :desc)
+        where(visibility: visibility).or(where(id: account.mentions.select(:status_id)))
       end
     end
 
diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb
index df399211c..50c4f6a04 100644
--- a/app/serializers/activitypub/activity_serializer.rb
+++ b/app/serializers/activitypub/activity_serializer.rb
@@ -5,6 +5,7 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
 
   has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce?
   attribute :proper_uri, key: :object, if: :announce?
+  attribute :atom_uri, if: :announce?
 
   def id
     ActivityPub::TagManager.instance.activity_uri_for(object)
@@ -34,6 +35,10 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
     ActivityPub::TagManager.instance.uri_for(object.proper)
   end
 
+  def atom_uri
+    OStatus::TagManager.instance.uri_for(object)
+  end
+
   def announce?
     object.reblog?
   end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index cf3d78683..7029c4d75 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -72,6 +72,8 @@ class FetchLinkCardService < BaseService
   def attempt_oembed
     response = OEmbed::Providers.get(@url)
 
+    return false unless response.respond_to?(:type)
+
     @card.type          = response.type
     @card.title         = response.respond_to?(:title)         ? response.title         : ''
     @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
@@ -113,7 +115,7 @@ class FetchLinkCardService < BaseService
     detector.strip_tags = true
 
     guess = detector.detect(html, response.charset)
-    page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding))
+    page  = Nokogiri::HTML(html, nil, guess&.fetch(:encoding, nil))
 
     if meta_property(page, 'twitter:player')
       @card.type   = :video
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index cacf6ba51..9c009335b 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -40,6 +40,6 @@ class FetchRemoteStatusService < BaseService
   end
 
   def confirmed_domain?(domain, account)
-    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url).normalized_host).zero?
+    account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url || account.uri).normalized_host).zero?
   end
 end
diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb
index af205c9c9..fabba8a3e 100644
--- a/app/services/send_interaction_service.rb
+++ b/app/services/send_interaction_service.rb
@@ -12,9 +12,11 @@ class SendInteractionService < BaseService
 
     return if !target_account.ostatus? || block_notification?
 
-    delivery = build_request.perform.flush
+    delivery = build_request.perform
 
     raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300
+
+    delivery.connection&.close
   end
 
   private
diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb
index 2d8af0203..2f725e2ec 100644
--- a/app/services/subscribe_service.rb
+++ b/app/services/subscribe_service.rb
@@ -6,7 +6,7 @@ class SubscribeService < BaseService
 
     @account        = account
     @account.secret = SecureRandom.hex
-    @response       = build_request.perform.flush
+    @response       = build_request.perform
 
     if response_failed_permanently?
       # We're not allowed to subscribe. Fail and move on.
@@ -20,6 +20,7 @@ class SubscribeService < BaseService
       # We need to retry at a later time. Fail loudly!
       raise Mastodon::UnexpectedResponseError, @response
     end
+    @response.connection&.close
   end
 
   private
diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb
index d84a5a530..01f5c6b7a 100644
--- a/app/services/unsubscribe_service.rb
+++ b/app/services/unsubscribe_service.rb
@@ -7,9 +7,10 @@ class UnsubscribeService < BaseService
     @account = account
 
     begin
-      @response = build_request.perform.flush
+      @response = build_request.perform
 
       Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success?
+      @response.connection&.close
     rescue HTTP::Error, OpenSSL::SSL::SSLError => e
       Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}"
     end
diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml
index 53263c43f..1fa64908c 100644
--- a/app/views/admin/custom_emojis/_custom_emoji.html.haml
+++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml
@@ -10,7 +10,7 @@
       = custom_emoji.domain
   %td
     - unless custom_emoji.local?
-      = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji), method: :post
+      = table_link_to 'copy', t('admin.custom_emojis.copy'), copy_admin_custom_emoji_path(custom_emoji, page: params[:page]), method: :post
   %td
     - if custom_emoji.disabled?
       = table_link_to 'power-off', t('admin.custom_emojis.enable'), enable_admin_custom_emoji_path(custom_emoji), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index 6266d3c0c..ea8b0faa3 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -7,31 +7,43 @@
   = render 'og'
 
 .landing-page.tag-page
-  .stripe
   .features
     .container
       #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } }
 
       .about-mastodon
-        .brand
-          = link_to root_url do
-            = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
+        .about-hashtag
+          .brand
+            = link_to root_url do
+              = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon'
 
-        %p= t 'about.about_hashtag_html', hashtag: @tag.name
+          %p= t 'about.about_hashtag_html', hashtag: @tag.name
 
-        .cta
-          = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
-          = link_to t('about.learn_more'), root_url, class: 'button button-alternative'
+          .cta
+            = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
+            = link_to t('about.learn_more'), root_url, class: 'button button-alternative'
 
         .features-list
           .features-list__row
             .text
+              %h6= t 'about.features.real_conversation_title'
+              = t 'about.features.real_conversation_body'
+            .visual
+              = fa_icon 'fw comments'
+          .features-list__row
+            .text
               %h6= t 'about.features.not_a_product_title'
               = t 'about.features.not_a_product_body'
             .visual
               = fa_icon 'fw users'
           .features-list__row
             .text
+              %h6= t 'about.features.within_reach_title'
+              = t 'about.features.within_reach_body'
+            .visual
+              = fa_icon 'fw mobile'
+          .features-list__row
+            .text
               %h6= t 'about.features.humane_approach_title'
               = t 'about.features.humane_approach_body'
             .visual
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 7b1e06a70..ae86e3dd2 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -16,6 +16,7 @@ class ActivityPub::DeliveryWorker
 
     raise Mastodon::UnexpectedResponseError, @response unless response_successful?
 
+    @response.connection&.close
     failure_tracker.track_success!
   rescue => e
     failure_tracker.track_failure!
@@ -31,7 +32,7 @@ class ActivityPub::DeliveryWorker
   end
 
   def perform_request
-    @response = build_request.perform.flush
+    @response = build_request.perform
   end
 
   def response_successful?
diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb
index c3506727b..a9174edd2 100644
--- a/app/workers/pubsubhubbub/delivery_worker.rb
+++ b/app/workers/pubsubhubbub/delivery_worker.rb
@@ -27,6 +27,7 @@ class Pubsubhubbub::DeliveryWorker
 
     raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful?
 
+    payload_delivery.connection&.close
     subscription.touch(:last_successful_delivery_at)
   end
 
@@ -37,7 +38,7 @@ class Pubsubhubbub::DeliveryWorker
   def callback_post_payload
     request = Request.new(:post, subscription.callback_url, body: payload)
     request.add_headers(headers)
-    request.perform.flush
+    request.perform
   end
 
   def blocked_domain?