about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/api/base_controller.rb15
-rw-r--r--app/controllers/api/v1/accounts/lists_controller.rb20
-rw-r--r--app/controllers/api/v1/lists_controller.rb38
-rw-r--r--app/controllers/api/web/push_subscriptions_controller.rb2
-rw-r--r--app/controllers/concerns/rate_limit_headers.rb3
-rw-r--r--app/helpers/settings_helper.rb1
-rw-r--r--app/javascript/mastodon/actions/favourites.js6
-rw-r--r--app/javascript/mastodon/actions/push_notifications.js11
-rw-r--r--app/javascript/mastodon/features/favourited_statuses/index.js10
-rw-r--r--app/javascript/mastodon/features/status/components/card.js10
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.js1
-rw-r--r--app/javascript/mastodon/features/video/index.js65
-rw-r--r--app/javascript/mastodon/locales/ar.json2
-rw-r--r--app/javascript/mastodon/locales/bg.json2
-rw-r--r--app/javascript/mastodon/locales/ca.json2
-rw-r--r--app/javascript/mastodon/locales/de.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/es.json2
-rw-r--r--app/javascript/mastodon/locales/fa.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json2
-rw-r--r--app/javascript/mastodon/locales/fr.json40
-rw-r--r--app/javascript/mastodon/locales/gl.json259
-rw-r--r--app/javascript/mastodon/locales/he.json2
-rw-r--r--app/javascript/mastodon/locales/hr.json2
-rw-r--r--app/javascript/mastodon/locales/hu.json2
-rw-r--r--app/javascript/mastodon/locales/id.json2
-rw-r--r--app/javascript/mastodon/locales/io.json2
-rw-r--r--app/javascript/mastodon/locales/it.json2
-rw-r--r--app/javascript/mastodon/locales/ja.json2
-rw-r--r--app/javascript/mastodon/locales/ko.json2
-rw-r--r--app/javascript/mastodon/locales/nl.json22
-rw-r--r--app/javascript/mastodon/locales/no.json2
-rw-r--r--app/javascript/mastodon/locales/oc.json60
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json32
-rw-r--r--app/javascript/mastodon/locales/pt.json2
-rw-r--r--app/javascript/mastodon/locales/ru.json2
-rw-r--r--app/javascript/mastodon/locales/sv.json2
-rw-r--r--app/javascript/mastodon/locales/th.json2
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/locales/uk.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_gl.json2
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json2
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json2
-rw-r--r--app/javascript/mastodon/middleware/sounds.js2
-rw-r--r--app/javascript/mastodon/reducers/status_lists.js12
-rw-r--r--app/javascript/mastodon/settings.js46
-rw-r--r--app/javascript/mastodon/web_push_subscription.js32
-rw-r--r--app/javascript/styles/mastodon/components.scss78
-rw-r--r--app/lib/activitypub/activity/create.rb22
-rw-r--r--app/lib/ostatus/activity/creation.rb21
-rw-r--r--app/models/account.rb3
-rw-r--r--app/models/list.rb8
-rw-r--r--app/models/preview_card.rb2
-rw-r--r--app/models/tag.rb2
-rw-r--r--app/services/account_search_service.rb2
-rw-r--r--app/services/fetch_link_card_service.rb2
-rw-r--r--app/services/resolve_remote_account_service.rb2
-rw-r--r--app/validators/status_pin_validator.rb8
-rw-r--r--app/views/stream_entries/_detailed_status.html.haml20
-rw-r--r--app/views/stream_entries/show.html.haml3
-rw-r--r--app/views/tags/show.html.haml7
62 files changed, 686 insertions, 235 deletions
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 7cfe8fe71..5983c0fbe 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -72,19 +72,4 @@ class Api::BaseController < ApplicationController
   def render_empty
     render json: {}, status: 200
   end
-
-  def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
-    if current_account.nil?
-      @reblogs_map    = {}
-      @favourites_map = {}
-      @mutes_map      = {}
-      return
-    end
-
-    status_ids       = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
-    conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
-    @reblogs_map     = Status.reblogs_map(status_ids, current_account)
-    @favourites_map  = Status.favourites_map(status_ids, current_account)
-    @mutes_map       = Status.mutes_map(conversation_ids, current_account)
-  end
 end
diff --git a/app/controllers/api/v1/accounts/lists_controller.rb b/app/controllers/api/v1/accounts/lists_controller.rb
new file mode 100644
index 000000000..a7ba89ce2
--- /dev/null
+++ b/app/controllers/api/v1/accounts/lists_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::ListsController < Api::BaseController
+  before_action -> { doorkeeper_authorize! :read }
+  before_action :require_user!
+  before_action :set_account
+
+  respond_to :json
+
+  def index
+    @lists = @account.lists.where(account: current_account)
+    render json: @lists, each_serializer: REST::ListSerializer
+  end
+
+  private
+
+  def set_account
+    @account = Account.find(params[:account_id])
+  end
+end
diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
index 9437373bd..180a91d81 100644
--- a/app/controllers/api/v1/lists_controller.rb
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -1,18 +1,14 @@
 # frozen_string_literal: true
 
 class Api::V1::ListsController < Api::BaseController
-  LISTS_LIMIT = 50
-
   before_action -> { doorkeeper_authorize! :read },    only: [:index, :show]
   before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
 
   before_action :require_user!
   before_action :set_list, except: [:index, :create]
 
-  after_action :insert_pagination_headers, only: :index
-
   def index
-    @lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
+    @lists = List.where(account: current_account).all
     render json: @lists, each_serializer: REST::ListSerializer
   end
 
@@ -44,36 +40,4 @@ class Api::V1::ListsController < Api::BaseController
   def list_params
     params.permit(:title)
   end
-
-  def insert_pagination_headers
-    set_pagination_headers(next_path, prev_path)
-  end
-
-  def next_path
-    if records_continue?
-      api_v1_lists_url pagination_params(max_id: pagination_max_id)
-    end
-  end
-
-  def prev_path
-    unless @lists.empty?
-      api_v1_lists_url pagination_params(since_id: pagination_since_id)
-    end
-  end
-
-  def pagination_max_id
-    @lists.last.id
-  end
-
-  def pagination_since_id
-    @lists.first.id
-  end
-
-  def records_continue?
-    @lists.size == limit_param(LISTS_LIMIT)
-  end
-
-  def pagination_params(core_params)
-    params.permit(:limit).merge(core_params)
-  end
 end
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index d66237feb..52e250d02 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -28,6 +28,8 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
       },
     }
 
+    data.deep_merge!(params[:data]) if params[:data]
+
     web_subscription = ::Web::PushSubscription.create!(
       endpoint: params[:subscription][:endpoint],
       key_p256dh: params[:subscription][:keys][:p256dh],
diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb
index 36cb91075..b79c558d8 100644
--- a/app/controllers/concerns/rate_limit_headers.rb
+++ b/app/controllers/concerns/rate_limit_headers.rb
@@ -44,7 +44,8 @@ module RateLimitHeaders
   end
 
   def api_throttle_data
-    request.env['rack.attack.throttle_data']['api']
+    most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
+    request.env['rack.attack.throttle_data'][most_limited_type]
   end
 
   def request_time
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index abce85812..1d4cb8a57 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -10,6 +10,7 @@ module SettingsHelper
     eo: 'Esperanto',
     es: 'Español',
     fa: 'فارسی',
+    gl: 'Galego',
     fi: 'Suomi',
     fr: 'Français',
     he: 'עברית',
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 09ce51fce..93094c526 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL    = 'FAVOURITED_STATUSES_EXPAND_FA
 
 export function fetchFavouritedStatuses() {
   return (dispatch, getState) => {
+    if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
+      return;
+    }
+
     dispatch(fetchFavouritedStatusesRequest());
 
     api(getState).get('/api/v1/favourites').then(response => {
@@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
   return (dispatch, getState) => {
     const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
 
-    if (url === null) {
+    if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
       return;
     }
 
diff --git a/app/javascript/mastodon/actions/push_notifications.js b/app/javascript/mastodon/actions/push_notifications.js
index 55661d2b0..de06385f9 100644
--- a/app/javascript/mastodon/actions/push_notifications.js
+++ b/app/javascript/mastodon/actions/push_notifications.js
@@ -1,4 +1,5 @@
 import axios from 'axios';
+import { pushNotificationsSetting } from '../settings';
 
 export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
 export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
@@ -42,11 +43,15 @@ export function saveSettings() {
     const state = getState().get('push_notifications');
     const subscription = state.get('subscription');
     const alerts = state.get('alerts');
+    const data = { alerts };
 
     axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
-      data: {
-        alerts,
-      },
+      data,
+    }).then(() => {
+      const me = getState().getIn(['meta', 'me']);
+      if (me) {
+        pushNotificationsSetting.set(me, data);
+      }
     });
   };
 }
diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js
index 1e1f5873c..67b107bc8 100644
--- a/app/javascript/mastodon/features/favourited_statuses/index.js
+++ b/app/javascript/mastodon/features/favourited_statuses/index.js
@@ -9,6 +9,7 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import StatusList from '../../components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import { debounce } from 'lodash';
 
 const messages = defineMessages({
   heading: { id: 'column.favourites', defaultMessage: 'Favourites' },
@@ -16,6 +17,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   statusIds: state.getIn(['status_lists', 'favourites', 'items']),
+  isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
   hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
 });
 
@@ -30,6 +32,7 @@ export default class Favourites extends ImmutablePureComponent {
     columnId: PropTypes.string,
     multiColumn: PropTypes.bool,
     hasMore: PropTypes.bool,
+    isLoading: PropTypes.bool,
   };
 
   componentWillMount () {
@@ -59,12 +62,12 @@ export default class Favourites extends ImmutablePureComponent {
     this.column = c;
   }
 
-  handleScrollToBottom = () => {
+  handleScrollToBottom = debounce(() => {
     this.props.dispatch(expandFavouritedStatuses());
-  }
+  }, 300, { leading: true })
 
   render () {
-    const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
+    const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
     const pinned = !!columnId;
 
     return (
@@ -85,6 +88,7 @@ export default class Favourites extends ImmutablePureComponent {
           statusIds={statusIds}
           scrollKey={`favourited_statuses-${columnId}`}
           hasMore={hasMore}
+          isLoading={isLoading}
           onScrollToBottom={this.handleScrollToBottom}
         />
       </Column>
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index d3e322c36..f7d980066 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -59,6 +59,8 @@ export default class Card extends React.PureComponent {
 
   renderLink () {
     const { card, maxDescription } = this.props;
+    const { width }  = this.state;
+    const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width);
 
     let image    = '';
     let provider = card.get('provider_name');
@@ -75,17 +77,15 @@ export default class Card extends React.PureComponent {
       provider = decodeIDNA(getHostname(card.get('url')));
     }
 
-    const className = classnames('status-card', {
-      'horizontal': card.get('width') > card.get('height'),
-    });
+    const className = classnames('status-card', { horizontal });
 
     return (
-      <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
         {image}
 
         <div className='status-card__content'>
           <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
-          <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
+          {!horizontal && <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>}
           <span className='status-card__host'>{provider}</span>
         </div>
       </a>
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index 1437deeb0..6a883759f 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -23,6 +23,7 @@ export default class VideoModal extends ImmutablePureComponent {
             src={media.get('url')}
             startTime={time}
             onCloseVideo={onClose}
+            detailed
             description={media.get('description')}
           />
         </div>
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 003bf23a8..0ee8bb6c8 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -17,6 +17,18 @@ const messages = defineMessages({
   exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
 });
 
+const formatTime = secondsNum => {
+  let hours   = Math.floor(secondsNum / 3600);
+  let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
+  let seconds = secondsNum - (hours * 3600) - (minutes * 60);
+
+  if (hours   < 10) hours   = '0' + hours;
+  if (minutes < 10) minutes = '0' + minutes;
+  if (seconds < 10) seconds = '0' + seconds;
+
+  return (hours === '00' ? '' : `${hours}:`) + `${minutes}:${seconds}`;
+};
+
 const findElementPosition = el => {
   let box;
 
@@ -83,11 +95,13 @@ export default class Video extends React.PureComponent {
     startTime: PropTypes.number,
     onOpenVideo: PropTypes.func,
     onCloseVideo: PropTypes.func,
+    detailed: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
   state = {
-    progress: 0,
+    currentTime: 0,
+    duration: 0,
     paused: true,
     dragging: false,
     fullscreen: false,
@@ -117,7 +131,10 @@ export default class Video extends React.PureComponent {
   }
 
   handleTimeUpdate = () => {
-    this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
+    this.setState({
+      currentTime: Math.floor(this.video.currentTime),
+      duration: Math.floor(this.video.duration),
+    });
   }
 
   handleMouseDown = e => {
@@ -143,8 +160,10 @@ export default class Video extends React.PureComponent {
 
   handleMouseMove = throttle(e => {
     const { x } = getPointerPosition(this.seek, e);
-    this.video.currentTime = this.video.duration * x;
-    this.setState({ progress: x * 100 });
+    const currentTime = Math.floor(this.video.duration * x);
+
+    this.video.currentTime = currentTime;
+    this.setState({ currentTime });
   }, 60);
 
   togglePlay = () => {
@@ -226,11 +245,12 @@ export default class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt } = this.props;
-    const { progress, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+    const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props;
+    const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
+    const progress = (currentTime / duration) * 100;
 
     return (
-      <div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+      <div className={classNames('video-player', { inactive: !revealed, detailed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
         <video
           ref={this.setVideoRef}
           src={src}
@@ -267,16 +287,27 @@ export default class Video extends React.PureComponent {
             />
           </div>
 
-          <div className='video-player__buttons left'>
-            <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
-            <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
-            {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
-          </div>
-
-          <div className='video-player__buttons right'>
-            {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
-            {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
-            <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
+          <div className='video-player__buttons-bar'>
+            <div className='video-player__buttons left'>
+              <button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
+              <button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
+
+              {!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
+
+              {(detailed || fullscreen) &&
+                <span>
+                  <span className='video-player__time-current'>{formatTime(currentTime)}</span>
+                  <span className='video-player__time-sep'>/</span>
+                  <span className='video-player__time-total'>{formatTime(duration)}</span>
+                </span>
+              }
+            </div>
+
+            <div className='video-player__buttons right'>
+              {(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
+              {onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>}
+              <button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
+            </div>
           </div>
         </div>
       </div>
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index a984b38d2..ec66a0027 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index d20120b11..1c04b3bfa 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Зареждане...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index bfa931fc0..f705937fd 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index b6d9e27a7..6354f18b6 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index f5154634c..0f766af6a 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -135,7 +135,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 619c7320a..9e66c379f 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Ŝarganta…",
   "media_gallery.toggle_visible": "Baskuli videblecon",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 411615744..6122a79ab 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index aa5c21feb..75057a7dd 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index db9319e2e..4ddc1cca7 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Ladataan...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 9b9469bc2..a7a8876d0 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -63,8 +63,8 @@
   "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_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.confirm": "Supprimer",
+  "confirmations.delete_list.message": "Êtes-vous sûr de vouloir supprimer définitivement cette liste ?",
   "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",
@@ -114,27 +114,27 @@
   "keyboard_shortcuts.description": "Description",
   "keyboard_shortcuts.down": "descendre dans la liste",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.favourite": "vers les favoris",
+  "keyboard_shortcuts.heading": "Raccourcis clavier",
+  "keyboard_shortcuts.hotkey": "Raccourci",
+  "keyboard_shortcuts.legend": "pour afficher cette légende",
+  "keyboard_shortcuts.mention": "pour mentionner l'auteur",
+  "keyboard_shortcuts.reply": "pour répondre",
   "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.toot": "pour démarrer un tout nouveau pouet",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.account.add": "Ajouter à la liste",
+  "lists.account.remove": "Supprimer de la liste",
+  "lists.delete": "Effacer la liste",
+  "lists.edit": "Éditer la liste",
+  "lists.new.create": "Ajouter une liste",
+  "lists.new.title_placeholder": "Titre de la nouvelle liste",
+  "lists.search": "Rechercher parmi les gens que vous suivez",
+  "lists.subheading": "Vos listes",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
@@ -145,8 +145,8 @@
   "navigation_bar.favourites": "Favoris",
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.info": "Plus d’informations",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
+  "navigation_bar.lists": "Listes",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
   "navigation_bar.pins": "Pouets épinglés",
@@ -241,7 +241,7 @@
   "tabs_bar.home": "Accueil",
   "tabs_bar.local_timeline": "Fil public local",
   "tabs_bar.notifications": "Notifications",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",
   "upload_area.title": "Glissez et déposez pour envoyer",
   "upload_button.label": "Joindre un média",
   "upload_form.description": "Décrire pour les malvoyants",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
new file mode 100644
index 000000000..bb0b1a9fd
--- /dev/null
+++ b/app/javascript/mastodon/locales/gl.json
@@ -0,0 +1,259 @@
+{
+  "account.block": "Bloquear @{name}",
+  "account.block_domain": "Ocultar calquer contido de {domain}",
+  "account.disclaimer_full": "A información inferior podería mostrar un perfil incompleto da usuaria.",
+  "account.edit_profile": "Editar perfil",
+  "account.follow": "Seguir",
+  "account.followers": "Seguidoras",
+  "account.follows": "Seguindo",
+  "account.follows_you": "Séguena",
+  "account.hide_reblogs": "Ocultar repeticións de @{name}",
+  "account.media": "Medios",
+  "account.mention": "Mencionar @{name}",
+  "account.moved_to": "{name} marchou a:",
+  "account.mute": "Acalar @{name}",
+  "account.mute_notifications": "Acalar as notificacións de @{name}",
+  "account.posts": "Publicacións",
+  "account.report": "Informar sobre @{name}",
+  "account.requested": "Agardando aceptación. Pulse para cancelar a solicitude de seguimento",
+  "account.share": "Compartir o perfil de @{name}",
+  "account.show_reblogs": "Mostrar repeticións de @{name}",
+  "account.unblock": "Desbloquear @{name}",
+  "account.unblock_domain": "Non ocultar {domain}",
+  "account.unfollow": "Non seguir",
+  "account.unmute": "Non acalar @{name}",
+  "account.unmute_notifications": "Desbloquear as notificacións de @{name}",
+  "account.view_full_profile": "Ver o perfil completo",
+  "boost_modal.combo": "Pulse {combo} para saltar esto a próxima vez",
+  "bundle_column_error.body": "Houbo un fallo mentras se cargaba este compoñente.",
+  "bundle_column_error.retry": "Inténteo de novo",
+  "bundle_column_error.title": "Fallo na rede",
+  "bundle_modal_error.close": "Pechar",
+  "bundle_modal_error.message": "Algo fallou mentras se cargaba este compoñente.",
+  "bundle_modal_error.retry": "Inténteo de novo",
+  "column.blocks": "Usuarias bloqueadas",
+  "column.community": "Liña temporal local",
+  "column.favourites": "Favoritas",
+  "column.follow_requests": "Peticións de seguimento",
+  "column.home": "Inicio",
+  "column.lists": "Lists",
+  "column.mutes": "Usuarias acaladas",
+  "column.notifications": "Notificacións",
+  "column.pins": "Mensaxes fixadas",
+  "column.public": "Liña temporal federada",
+  "column_back_button.label": "Atrás",
+  "column_header.hide_settings": "Agochar axustes",
+  "column_header.moveLeft_settings": "Mover a columna hacia a esquerda",
+  "column_header.moveRight_settings": "Mover a columna hacia a dereita",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostras axustes",
+  "column_header.unpin": "Soltar",
+  "column_subheading.navigation": "Navegación",
+  "column_subheading.settings": "Axustes",
+  "compose_form.lock_disclaimer": "A súa conta non está {locked}. Calquera pode seguila para ver as súas mensaxes só-para-seguidoras.",
+  "compose_form.lock_disclaimer.lock": "bloqueado",
+  "compose_form.placeholder": "A qué andas?",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive": "Marcar medios como sensibles",
+  "compose_form.spoiler": "Agochar texto detrás de un aviso",
+  "compose_form.spoiler_placeholder": "Escriba o aviso aquí",
+  "confirmation_modal.cancel": "Cancelar",
+  "confirmations.block.confirm": "Bloquear",
+  "confirmations.block.message": "Está segura de querer bloquear a {name}?",
+  "confirmations.delete.confirm": "Borrar",
+  "confirmations.delete.message": "Está segura de que quere eliminar este estado?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Agochar un dominio completo",
+  "confirmations.domain_block.message": "Realmente está segura de que quere bloquear por completo o dominio {domain}? Normalmente é suficiente, e preferible, bloquear de xeito selectivo varios elementos.",
+  "confirmations.mute.confirm": "Acalar",
+  "confirmations.mute.message": "Está segura de que quere acalar a {name}?",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "Quere deixar de seguir a {name}?",
+  "embed.instructions": "Copie o código inferior para incrustar no seu sitio web este estado.",
+  "embed.preview": "Así será mostrado:",
+  "emoji_button.activity": "Actividade",
+  "emoji_button.custom": "Personalizado",
+  "emoji_button.flags": "Marcas",
+  "emoji_button.food": "Comida e Bebida",
+  "emoji_button.label": "Insertar emoji",
+  "emoji_button.nature": "Natureza",
+  "emoji_button.not_found": "Sen emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Obxetos",
+  "emoji_button.people": "Xente",
+  "emoji_button.recent": "Utilizadas con frecuencia",
+  "emoji_button.search": "Buscar...",
+  "emoji_button.search_results": "Resultados da busca",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viaxes e Lugares",
+  "empty_column.community": "A liña temporal local está baldeira. Escriba algo de xeito público para que rule!",
+  "empty_column.hashtag": "Aínda non hai nada con esta etiqueta.",
+  "empty_column.home": "A súa liña temporal de inicio está baldeira! Visite {public} ou utilice a busca para atopar outras usuarias.",
+  "empty_column.home.public_timeline": "a liña temporal pública",
+  "empty_column.list": "Aínda non hai nada en esta lista.",
+  "empty_column.notifications": "Aínda non ten notificacións. Interactúe con outras para iniciar unha conversa.",
+  "empty_column.public": "Nada por aquí! Escriba algo de xeito público, ou siga manualmente usuarias de outras instancias para ir enchéndoa",
+  "follow_request.authorize": "Autorizar",
+  "follow_request.reject": "Rexeitar",
+  "getting_started.appsshort": "Aplicacións",
+  "getting_started.faq": "PMF",
+  "getting_started.heading": "Comezando",
+  "getting_started.open_source_notice": "Mastodon é software de código aberto. Pode contribuír ou informar de fallos en GitHub en {github}.",
+  "getting_started.userguide": "Guía de usuaria",
+  "home.column_settings.advanced": "Avanzado",
+  "home.column_settings.basic": "Básico",
+  "home.column_settings.filter_regex": "Filtrar expresións regulares",
+  "home.column_settings.show_reblogs": "Mostrar repeticións",
+  "home.column_settings.show_replies": "Mostrar respostas",
+  "home.settings": "Axustes da columna",
+  "keyboard_shortcuts.back": "voltar atrás",
+  "keyboard_shortcuts.boost": "repetir",
+  "keyboard_shortcuts.column": "destacar un estado en unha das columnas",
+  "keyboard_shortcuts.compose": "Foco no área de escritura",
+  "keyboard_shortcuts.description": "Descrición",
+  "keyboard_shortcuts.down": "ir hacia abaixo na lista",
+  "keyboard_shortcuts.enter": "abrir estado",
+  "keyboard_shortcuts.favourite": "marcar como favorito",
+  "keyboard_shortcuts.heading": "Atallos do teclado",
+  "keyboard_shortcuts.hotkey": "Tecla de acceso directo",
+  "keyboard_shortcuts.legend": "para mostrar esta lenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para centrar a busca",
+  "keyboard_shortcuts.toot": "escribir un toot novo",
+  "keyboard_shortcuts.unfocus": "quitar o foco do área de escritura/busca",
+  "keyboard_shortcuts.up": "ir hacia arriba na lista",
+  "lightbox.close": "Fechar",
+  "lightbox.next": "Seguinte",
+  "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among people you follow",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "Cargando...",
+  "media_gallery.toggle_visible": "Dar visibilidade",
+  "missing_indicator.label": "Non atopado",
+  "mute_modal.hide_notifications": "Esconder notificacións deste usuario?",
+  "navigation_bar.blocks": "Usuarios bloqueados",
+  "navigation_bar.community_timeline": "Liña temporal local",
+  "navigation_bar.edit_profile": "Editar perfil",
+  "navigation_bar.favourites": "Favoritas",
+  "navigation_bar.follow_requests": "Peticións de seguimento",
+  "navigation_bar.info": "Sobre esta instancia",
+  "navigation_bar.keyboard_shortcuts": "Atallos do teclado",
+  "navigation_bar.lists": "Lists",
+  "navigation_bar.logout": "Sair",
+  "navigation_bar.mutes": "Usuarias acaladas",
+  "navigation_bar.pins": "Mensaxes fixadas",
+  "navigation_bar.preferences": "Preferencias",
+  "navigation_bar.public_timeline": "Liña temporal federada",
+  "notification.favourite": "{name} marcou como favorito o seu estado",
+  "notification.follow": "{name} está a seguila",
+  "notification.mention": "{name} mencionoute",
+  "notification.reblog": "{name} promocionou o seu estado",
+  "notifications.clear": "Limpar notificacións",
+  "notifications.clear_confirmation": "Estás seguro de que queres limpar permanentemente todas as túas notificacións?",
+  "notifications.column_settings.alert": "Notificacións de escritorio",
+  "notifications.column_settings.favourite": "Favoritas:",
+  "notifications.column_settings.follow": "Novos seguidores:",
+  "notifications.column_settings.mention": "Mencións:",
+  "notifications.column_settings.push": "Enviar notificacións",
+  "notifications.column_settings.push_meta": "Este aparello",
+  "notifications.column_settings.reblog": "Promocións:",
+  "notifications.column_settings.show": "Mostrar en columna",
+  "notifications.column_settings.sound": "Reproducir son",
+  "onboarding.done": "Feito",
+  "onboarding.next": "Seguinte",
+  "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.",
+  "onboarding.page_four.home": "A liña de tempo local mostra as publicacións das persoas que segues.",
+  "onboarding.page_four.notifications": "A columna de notificacións mostra cando alguén interactúa contigo.",
+  "onboarding.page_one.federation": "Mastodon é unha rede de servidores independentes que se unen para facer unha rede social máis grande. Chamamos instancias a estes servidores.",
+  "onboarding.page_one.handle": "Estás en {domain}, polo que o teu nome de usuario completo é {handle}",
+  "onboarding.page_one.welcome": "Benvido a Mastodon!",
+  "onboarding.page_six.admin": "O administrador da túa instancia é {admin}.",
+  "onboarding.page_six.almost_done": "Case feito...",
+  "onboarding.page_six.appetoot": "Que tootes ben!",
+  "onboarding.page_six.apps_available": "Hai {apps} dispoñíbeis para iOS, Android e outras plataformas.",
+  "onboarding.page_six.github": "Mastodon é un software gratuito e de código aberto. Pode informar de erros, solicitar novas funcionalidades ou contribuír ao código en {github}.",
+  "onboarding.page_six.guidelines": "directrices da comunidade",
+  "onboarding.page_six.read_guidelines": "Por favor, le as {guidelines} do {domain}!",
+  "onboarding.page_six.various_app": "aplicacións móbiles",
+  "onboarding.page_three.profile": "Edita o teu perfil para cambiar o teu avatar, bio e nome. Alí, tamén atoparás outras preferencias.",
+  "onboarding.page_three.search": "Utilice a barra de busca para atopar xente e descubrir etiquetas, como {illustration} e {introductions}. Para atopar unha usuaria que non está en esta instancia utilice o seu enderezo completo.",
+  "onboarding.page_two.compose": "Escriba mensaxes desde a columna de composición. Pode subir imaxes, mudar as opcións de intimidade e engadir avisos sobre o contido coas iconas inferiores.",
+  "onboarding.skip": "Saltar",
+  "privacy.change": "Axustar a intimidade do estado",
+  "privacy.direct.long": "Enviar exclusivamente as usuarias mencionadas",
+  "privacy.direct.short": "Directa",
+  "privacy.private.long": "Enviar só as seguidoras",
+  "privacy.private.short": "Só-seguidoras",
+  "privacy.public.long": "Publicar na liña temporal pública",
+  "privacy.public.short": "Pública",
+  "privacy.unlisted.long": "Non publicar en liñas temporais públicas",
+  "privacy.unlisted.short": "Non listada",
+  "relative_time.days": "{number}d",
+  "relative_time.hours": "{number}h",
+  "relative_time.just_now": "agora",
+  "relative_time.minutes": "{number}m",
+  "relative_time.seconds": "{number}s",
+  "reply_indicator.cancel": "Cancelar",
+  "report.placeholder": "Comentarios adicionais",
+  "report.submit": "Enviar",
+  "report.target": "Informar {target}",
+  "search.placeholder": "Buscar",
+  "search_popout.search_format": "Formato de busca avanzada",
+  "search_popout.tips.hashtag": "etiqueta",
+  "search_popout.tips.status": "estado",
+  "search_popout.tips.text": "Texto simple devolve coincidencias con nomes públicos, nomes de usuaria e etiquetas",
+  "search_popout.tips.user": "usuaria",
+  "search_results.total": "{count, number} {count,plural,one {result} outros {results}}",
+  "standalone.public_title": "Ollada dentro...",
+  "status.cannot_reblog": "Esta mensaxe non pode ser promocionada",
+  "status.delete": "Eliminar",
+  "status.embed": "Incrustar",
+  "status.favourite": "Favorita",
+  "status.load_more": "Cargar máis",
+  "status.media_hidden": "Medios ocultos",
+  "status.mention": "Mencionar @{name}",
+  "status.more": "Máis",
+  "status.mute_conversation": "Acalar conversa",
+  "status.open": "Expandir este estado",
+  "status.pin": "Fixar no perfil",
+  "status.reblog": "Promocionar",
+  "status.reblogged_by": "{name} promocionado",
+  "status.reply": "Resposta",
+  "status.replyAll": "Resposta a conversa",
+  "status.report": "Informar @{name}",
+  "status.sensitive_toggle": "Pulse para ver",
+  "status.sensitive_warning": "Contido sensible",
+  "status.share": "Compartir",
+  "status.show_less": "Mostrar menos",
+  "status.show_more": "Mostrar máis",
+  "status.unmute_conversation": "Non acalar a conversa",
+  "status.unpin": "Despegar do perfil",
+  "tabs_bar.compose": "Compoñer",
+  "tabs_bar.federated_timeline": "Federado",
+  "tabs_bar.home": "Inicio",
+  "tabs_bar.local_timeline": "Local",
+  "tabs_bar.notifications": "Notificacións",
+  "ui.beforeunload": "O borrador perderase se sae de Mastodon.",
+  "upload_area.title": "Arrastre e solte para subir",
+  "upload_button.label": "Engadir medios",
+  "upload_form.description": "Describa para deficientes visuais",
+  "upload_form.undo": "Desfacer",
+  "upload_progress.label": "Subindo...",
+  "video.close": "Pechar video",
+  "video.exit_fullscreen": "Saír da pantalla completa",
+  "video.expand": "Expandir vídeo",
+  "video.fullscreen": "Pantalla completa",
+  "video.hide": "Agochar vídeo",
+  "video.mute": "Acalar son",
+  "video.pause": "Pausar",
+  "video.play": "Reproducir",
+  "video.unmute": "Permitir son"
+}
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index ec1e30dd5..5444c8e34 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "טוען...",
   "media_gallery.toggle_visible": "נראה\\בלתי נראה",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index c21482670..f70c66223 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Učitavam...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 71dd810b6..7cb816fe9 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 744423e78..429b77182 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index b1523e626..3e5c8edb9 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 9a2d320fd..e2ad1632a 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Carico...",
   "media_gallery.toggle_visible": "Imposta visibilità",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 125618bf2..40ad66a7f 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -24,7 +24,7 @@
   "account.unmute": "ミュート解除",
   "account.unmute_notifications": "@{name}さんからの通知を受け取らない",
   "account.view_full_profile": "全ての情報を見る",
-  "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+  "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
   "bundle_column_error.retry": "再試行",
   "bundle_column_error.title": "ネットワークエラー",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 3f47baa76..472a52a99 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 26e86308d..c290ed767 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -107,22 +107,22 @@
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
   "home.settings": "Kolom-instellingen",
-  "keyboard_shortcuts.back": "om terug te navigeren",
+  "keyboard_shortcuts.back": "om terug te gaan",
   "keyboard_shortcuts.boost": "om te boosten",
-  "keyboard_shortcuts.column": "om te focussen op een status in één van de kolommen",
-  "keyboard_shortcuts.compose": "om te focussen op het toot tekstvak",
-  "keyboard_shortcuts.description": "Description",
+  "keyboard_shortcuts.column": "om op een toot te focussen in één van de kolommen",
+  "keyboard_shortcuts.compose": "om het tekstvak voor toots te focussen",
+  "keyboard_shortcuts.description": "Omschrijving",
   "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "om het te markeren als favoriet",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.favourite": "om als favoriet te markeren",
+  "keyboard_shortcuts.heading": "Sneltoetsen",
   "keyboard_shortcuts.hotkey": "Sneltoets",
   "keyboard_shortcuts.legend": "om deze legenda weer te geven",
   "keyboard_shortcuts.mention": "om de auteur te vermelden",
-  "keyboard_shortcuts.reply": "om te antwoorden",
-  "keyboard_shortcuts.search": "om te focussen op zoeken",
+  "keyboard_shortcuts.reply": "om te reageren",
+  "keyboard_shortcuts.search": "om het zoekvak te focussen",
   "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
-  "keyboard_shortcuts.unfocus": "om te ontfocussen van het toot tekstvak/zoeken",
+  "keyboard_shortcuts.unfocus": "om het tekst- en zoekvak te ontfocussen",
   "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
@@ -204,7 +204,7 @@
   "reply_indicator.cancel": "Annuleren",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
-  "report.target": "Rapporteren van",
+  "report.target": "Rapporteer {target}",
   "search.placeholder": "Zoeken",
   "search_popout.search_format": "Geavanceerd zoeken",
   "search_popout.tips.hashtag": "hashtag",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 233b6c946..bf2b6259a 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Laster...",
   "media_gallery.toggle_visible": "Veksle synlighet",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index ec7202ff6..f8b4751d6 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -10,22 +10,22 @@
   "account.hide_reblogs": "Rescondre los partages de @{name}",
   "account.media": "Mèdias",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} a mudat los catons a : ",
+  "account.moved_to": "{name} a mudat los catons a :",
   "account.mute": "Rescondre @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Rescondre las notificacions de @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
-  "account.requested": "Invitacion mandada. Clicatz per anullar.",
+  "account.requested": "Invitacion mandada. Clicatz per anullar",
   "account.share": "Partejar lo perfil a @{name}",
   "account.show_reblogs": "Mostrar los partages de @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Mostrar las notificacions de @{name}",
   "account.view_full_profile": "Veire lo perfil complet",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
-  "bundle_column_error.body": "Quicòm a fach meuca pendent lo cargament d’aqueste compausant.",
+  "bundle_column_error.body": "Quicòm a fach mèuca pendent lo cargament d’aqueste compausant.",
   "bundle_column_error.retry": "Tornar ensajar",
   "bundle_column_error.title": "Error de ret",
   "bundle_modal_error.close": "Tampar",
@@ -36,7 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
-  "column.lists": "Lists",
+  "column.lists": "Listas",
   "column.mutes": "Personas rescondudas",
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
@@ -63,8 +63,8 @@
   "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
   "confirmations.delete.confirm": "Escafar",
   "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
-  "confirmations.delete_list.confirm": "Delete",
-  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.delete_list.confirm": "Suprimir",
+  "confirmations.delete_list.message": "Sètz segur de voler suprimir aquesta lista per totjorn ?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Rescondre",
@@ -72,7 +72,7 @@
   "confirmations.unfollow.confirm": "Quitar de sègre",
   "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
   "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
-  "embed.preview": "Semblarà aquò : ",
+  "embed.preview": "Semblarà aquò :",
   "emoji_button.activity": "Activitats",
   "emoji_button.custom": "Personalizats",
   "emoji_button.flags": "Drapèus",
@@ -84,16 +84,16 @@
   "emoji_button.people": "Gents",
   "emoji_button.recent": "Sovent utilizats",
   "emoji_button.search": "Cercar…",
-  "emoji_button.search_results": "Resultat de recèrca",
+  "emoji_button.search_results": "Resultats de recèrca",
   "emoji_button.symbols": "Simbòls",
   "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.hashtag": "I a pas encara de contengut ligat a aquesta etiqueta.",
   "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.public_timeline": "lo flux public",
   "empty_column.list": "I a pas res dins la lista pel moment.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
-  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
+  "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Regetar",
   "getting_started.appsshort": "Apps",
@@ -116,7 +116,7 @@
   "keyboard_shortcuts.enter": "per dobrir los estatuts",
   "keyboard_shortcuts.favourite": "per apondre als favorits",
   "keyboard_shortcuts.heading": "Acorchis clavièr",
-  "keyboard_shortcuts.hotkey": "Clau",
+  "keyboard_shortcuts.hotkey": "Acorchis",
   "keyboard_shortcuts.legend": "per mostrar aquesta legenda",
   "keyboard_shortcuts.mention": "per mencionar l’autor",
   "keyboard_shortcuts.reply": "per respondre",
@@ -127,35 +127,35 @@
   "lightbox.close": "Tampar",
   "lightbox.next": "Seguent",
   "lightbox.previous": "Precedent",
-  "lists.account.add": "Add to list",
-  "lists.account.remove": "Remove from list",
-  "lists.delete": "Delete list",
-  "lists.edit": "Edit list",
-  "lists.new.create": "Add list",
-  "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
-  "lists.subheading": "Your lists",
+  "lists.account.add": "Ajustar a la lista",
+  "lists.account.remove": "Levar de la lista",
+  "lists.delete": "Suprimir la lista",
+  "lists.edit": "Modificar la lista",
+  "lists.new.create": "Ajustar una lista",
+  "lists.new.title_placeholder": "Títol de la nòva lista",
+  "lists.search": "Cercar demest lo monde que seguètz",
+  "lists.subheading": "Vòstras listas",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
   "navigation_bar.blocks": "Personas blocadas",
   "navigation_bar.community_timeline": "Flux public local",
   "navigation_bar.edit_profile": "Modificar lo perfil",
   "navigation_bar.favourites": "Favorits",
-  "navigation_bar.follow_requests": "Demandas d'abonament",
+  "navigation_bar.follow_requests": "Demandas d’abonament",
   "navigation_bar.info": "Mai informacions",
   "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
-  "navigation_bar.lists": "Lists",
+  "navigation_bar.lists": "Listas",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.pins": "Tuts penjats",
   "navigation_bar.preferences": "Preferéncias",
   "navigation_bar.public_timeline": "Flux public global",
-  "notification.favourite": "{name} a ajustat a sos favorits :",
+  "notification.favourite": "{name} a ajustat a sos favorits",
   "notification.follow": "{name} vos sèc",
-  "notification.mention": "{name} vos a mencionat :",
-  "notification.reblog": "{name} a partejat vòstre estatut :",
+  "notification.mention": "{name} vos a mencionat",
+  "notification.reblog": "{name} a partejat vòstre estatut",
   "notifications.clear": "Escafar",
   "notifications.clear_confirmation": "Volètz vertadièrament escafar totas vòstras las notificacions ?",
   "notifications.column_settings.alert": "Notificacions localas",
@@ -171,7 +171,7 @@
   "onboarding.next": "Seguent",
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
   "onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
-  "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
+  "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos.",
   "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
   "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
   "onboarding.page_one.welcome": "Benvengut a Mastodon !",
@@ -209,7 +209,7 @@
   "search_popout.search_format": "Format recèrca avançada",
   "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "estatut",
-  "search_popout.tips.text": "Tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
+  "search_popout.tips.text": "Lo tèxt brut tòrna escais, noms d’utilizaire e etiquetas correspondents",
   "search_popout.tips.user": "utilizaire",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "standalone.public_title": "Una ulhada dedins…",
@@ -225,7 +225,7 @@
   "status.open": "Desplegar aqueste estatut",
   "status.pin": "Penjar al perfil",
   "status.reblog": "Partejar",
-  "status.reblogged_by": "{name} a partejat :",
+  "status.reblogged_by": "{name} a partejat",
   "status.reply": "Respondre",
   "status.replyAll": "Respondre a la conversacion",
   "status.report": "Senhalar @{name}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 1df27d536..6bac65865 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -109,21 +109,21 @@
   "home.settings": "Configurações de colunas",
   "keyboard_shortcuts.back": "para navegar de volta",
   "keyboard_shortcuts.boost": "para compartilhar",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.column": "Focar um status em uma das colunas",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "para mover para baixo na lista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Atalho",
+  "keyboard_shortcuts.legend": "para mostrar essa legenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para focar a pesquisa",
+  "keyboard_shortcuts.toot": "para compor um novo toot",
+  "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa",
+  "keyboard_shortcuts.up": "para mover para cima na lista",
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
@@ -133,19 +133,19 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Esconder notificações deste usuário?",
   "navigation_bar.blocks": "Usuários bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
   "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
@@ -220,7 +220,7 @@
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Mídia escondida",
   "status.mention": "Mencionar @{name}",
-  "status.more": "More",
+  "status.more": "Mais",
   "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Fixar no perfil",
@@ -241,7 +241,7 @@
   "tabs_bar.home": "Página inicial",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Seu rascunho será perdido se você sair do Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar mídia",
   "upload_form.description": "Descreva a imagem para deficientes visuais",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 3d3e19571..728fb3a10 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 0aef2d9df..e9925b675 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 53090452f..9d9646509 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 2f064a193..cc18a6096 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index be8103d1c..c51f3e417 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Yükleniyor...",
   "media_gallery.toggle_visible": "Görünürlüğü değiştir",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 273661462..86c0ce76d 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
diff --git a/app/javascript/mastodon/locales/whitelist_gl.json b/app/javascript/mastodon/locales/whitelist_gl.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_gl.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index dbb9584c6..15a68c915 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 0b05a83cd..1bdc883a8 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -133,7 +133,7 @@
   "lists.edit": "Edit list",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
-  "lists.search": "Search among follows",
+  "lists.search": "Search among people you follow",
   "lists.subheading": "Your lists",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
diff --git a/app/javascript/mastodon/middleware/sounds.js b/app/javascript/mastodon/middleware/sounds.js
index 3d1e3eaba..9f1bc02b9 100644
--- a/app/javascript/mastodon/middleware/sounds.js
+++ b/app/javascript/mastodon/middleware/sounds.js
@@ -15,7 +15,7 @@ const play = audio => {
     if (typeof audio.fastSeek === 'function') {
       audio.fastSeek(0);
     } else {
-      audio.seek(0);
+      audio.currentTime = 0;
     }
   }
 
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index c4aeb338f..6c5f33557 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -1,6 +1,10 @@
 import {
+  FAVOURITED_STATUSES_FETCH_REQUEST,
   FAVOURITED_STATUSES_FETCH_SUCCESS,
+  FAVOURITED_STATUSES_FETCH_FAIL,
+  FAVOURITED_STATUSES_EXPAND_REQUEST,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
+  FAVOURITED_STATUSES_EXPAND_FAIL,
 } from '../actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
@@ -30,6 +34,7 @@ const normalizeList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
     map.set('loaded', true);
+    map.set('isLoading', false);
     map.set('items', ImmutableList(statuses.map(item => item.id)));
   }));
 };
@@ -37,6 +42,7 @@ const normalizeList = (state, listType, statuses, next) => {
 const appendToList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
+    map.set('isLoading', false);
     map.set('items', map.get('items').concat(statuses.map(item => item.id)));
   }));
 };
@@ -55,6 +61,12 @@ const removeOneFromList = (state, listType, status) => {
 
 export default function statusLists(state = initialState, action) {
   switch(action.type) {
+  case FAVOURITED_STATUSES_FETCH_REQUEST:
+  case FAVOURITED_STATUSES_EXPAND_REQUEST:
+    return state.setIn(['favourites', 'isLoading'], true);
+  case FAVOURITED_STATUSES_FETCH_FAIL:
+  case FAVOURITED_STATUSES_EXPAND_FAIL:
+    return state.setIn(['favourites', 'isLoading'], false);
   case FAVOURITED_STATUSES_FETCH_SUCCESS:
     return normalizeList(state, 'favourites', action.statuses, action.next);
   case FAVOURITED_STATUSES_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js
new file mode 100644
index 000000000..dbd969cb1
--- /dev/null
+++ b/app/javascript/mastodon/settings.js
@@ -0,0 +1,46 @@
+export default class Settings {
+
+  constructor(keyBase = null) {
+    this.keyBase = keyBase;
+  }
+
+  generateKey(id) {
+    return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
+  }
+
+  set(id, data) {
+    const key = this.generateKey(id);
+    try {
+      const encodedData = JSON.stringify(data);
+      localStorage.setItem(key, encodedData);
+      return data;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  get(id) {
+    const key = this.generateKey(id);
+    try {
+      const rawData = localStorage.getItem(key);
+      return JSON.parse(rawData);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  remove(id) {
+    const data = this.get(id);
+    if (data) {
+      const key = this.generateKey(id);
+      try {
+        localStorage.removeItem(key);
+      } catch (e) {
+      }
+    }
+    return data;
+  }
+
+}
+
+export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
diff --git a/app/javascript/mastodon/web_push_subscription.js b/app/javascript/mastodon/web_push_subscription.js
index 3dbed09ea..17aca4060 100644
--- a/app/javascript/mastodon/web_push_subscription.js
+++ b/app/javascript/mastodon/web_push_subscription.js
@@ -1,6 +1,7 @@
 import axios from 'axios';
 import { store } from './containers/mastodon';
 import { setBrowserSupport, setSubscription, clearSubscription } from './actions/push_notifications';
+import { pushNotificationsSetting } from './settings';
 
 // Taken from https://www.npmjs.com/package/web-push
 const urlBase64ToUint8Array = (base64String) => {
@@ -35,16 +36,33 @@ const subscribe = (registration) =>
 const unsubscribe = ({ registration, subscription }) =>
   subscription ? subscription.unsubscribe().then(() => registration) : registration;
 
-const sendSubscriptionToBackend = (subscription) =>
-  axios.post('/api/web/push_subscriptions', {
-    subscription,
-  }).then(response => response.data);
+const sendSubscriptionToBackend = (subscription) => {
+  const params = { subscription };
+
+  const me = store.getState().getIn(['meta', 'me']);
+  if (me) {
+    const data = pushNotificationsSetting.get(me);
+    if (data) {
+      params.data = data;
+    }
+  }
+
+  return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
+};
 
 // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
 const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
 
 export function register () {
   store.dispatch(setBrowserSupport(supportsPushNotifications));
+  const me = store.getState().getIn(['meta', 'me']);
+
+  if (me && !pushNotificationsSetting.get(me)) {
+    const alerts = store.getState().getIn(['push_notifications', 'alerts']);
+    if (alerts) {
+      pushNotificationsSetting.set(me, { alerts: alerts });
+    }
+  }
 
   if (supportsPushNotifications) {
     if (!getApplicationServerKey()) {
@@ -79,6 +97,9 @@ export function register () {
         // it means that the backend subscription is valid (and was set during hydration)
         if (!(subscription instanceof PushSubscription)) {
           store.dispatch(setSubscription(subscription));
+          if (me) {
+            pushNotificationsSetting.set(me, { alerts: subscription.alerts });
+          }
         }
       })
       .catch(error => {
@@ -90,6 +111,9 @@ export function register () {
 
         // Clear alerts and hide UI settings
         store.dispatch(clearSubscription());
+        if (me) {
+          pushNotificationsSetting.remove(me);
+        }
 
         try {
           getRegistration()
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 17322264e..da789ba06 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -803,7 +803,7 @@
     .emojione {
       width: 24px;
       height: 24px;
-      margin: -3px 0 0;
+      margin: -1px 0 0;
     }
   }
 
@@ -2273,14 +2273,19 @@ button.icon-button.active i.fa-retweet {
   .status-card__image-image {
     border-radius: 4px 4px 0 0;
   }
+
+  .status-card__title {
+    white-space: inherit;
+  }
 }
 
 .status-card__image-image {
   border-radius: 4px 0 0 4px;
   display: block;
-  height: auto;
   margin: 0;
   width: 100%;
+  height: 100%;
+  object-fit: cover;
 }
 
 .load-more {
@@ -3998,6 +4003,7 @@ button.icon-button.active i.fa-retweet {
   position: relative;
   background: $base-shadow-color;
   max-width: 100%;
+  border-radius: 4px;
 
   video {
     height: 100%;
@@ -4032,8 +4038,8 @@ button.icon-button.active i.fa-retweet {
     left: 0;
     right: 0;
     box-sizing: border-box;
-    background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
-    padding: 0 10px;
+    background: linear-gradient(0deg, rgba($base-shadow-color, 0.85) 0, rgba($base-shadow-color, 0.45) 60%, transparent);
+    padding: 0 15px;
     opacity: 0;
     transition: opacity .1s ease;
 
@@ -4086,40 +4092,67 @@ button.icon-button.active i.fa-retweet {
     }
   }
 
-  &__buttons {
+  &__buttons-bar {
+    display: flex;
+    justify-content: space-between;
     padding-bottom: 10px;
+  }
+
+  &__buttons {
     font-size: 16px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
 
     &.left {
-      float: left;
-
       button {
-        padding-right: 10px;
+        padding-left: 0;
       }
     }
 
     &.right {
-      float: right;
-
       button {
-        padding-left: 10px;
+        padding-right: 0;
       }
     }
 
     button {
       background: transparent;
-      padding: 0;
+      padding: 2px 10px;
+      font-size: 16px;
       border: 0;
-      color: $white;
+      color: rgba($white, 0.75);
 
       &:active,
       &:hover,
       &:focus {
-        color: $ui-highlight-color;
+        color: $white;
       }
     }
   }
 
+  &__time-sep,
+  &__time-total,
+  &__time-current {
+    font-size: 14px;
+    font-weight: 500;
+  }
+
+  &__time-current {
+    color: $white;
+    margin-left: 10px;
+  }
+
+  &__time-sep {
+    display: inline-block;
+    margin: 0 6px;
+  }
+
+  &__time-sep,
+  &__time-total {
+    color: $white;
+  }
+
   &__seek {
     cursor: pointer;
     height: 24px;
@@ -4129,6 +4162,7 @@ button.icon-button.active i.fa-retweet {
       content: "";
       width: 100%;
       background: rgba($white, 0.35);
+      border-radius: 4px;
       display: block;
       position: absolute;
       height: 4px;
@@ -4140,8 +4174,9 @@ button.icon-button.active i.fa-retweet {
       display: block;
       position: absolute;
       height: 4px;
+      border-radius: 4px;
       top: 10px;
-      background: $ui-highlight-color;
+      background: lighten($ui-highlight-color, 8%);
     }
 
     &__buffer {
@@ -4158,7 +4193,8 @@ button.icon-button.active i.fa-retweet {
       top: 6px;
       margin-left: -6px;
       transition: opacity .1s ease;
-      background: $ui-highlight-color;
+      background: lighten($ui-highlight-color, 8%);
+      box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
       pointer-events: none;
 
       &.active {
@@ -4172,6 +4208,16 @@ button.icon-button.active i.fa-retweet {
       }
     }
   }
+
+  &.detailed,
+  &.fullscreen {
+    .video-player__buttons {
+      button {
+        padding-top: 10px;
+        padding-bottom: 10px;
+      }
+    }
+  }
 }
 
 .media-spoiler-video {
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 31e0abe39..3a985c19b 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -20,11 +20,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
   private
 
   def process_status
+    media_attachments = process_attachments
+
     ApplicationRecord.transaction do
       @status = Status.create!(status_params)
 
       process_tags(@status)
-      process_attachments(@status)
+      attach_media(@status, media_attachments)
     end
 
     resolve_thread(@status)
@@ -105,22 +107,36 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     emoji.save
   end
 
-  def process_attachments(status)
+  def process_attachments
     return if @object['attachment'].nil?
 
+    media_attachments = []
+
     as_array(@object['attachment']).each do |attachment|
       next if unsupported_media_type?(attachment['mediaType']) || attachment['url'].blank?
 
       href             = Addressable::URI.parse(attachment['url']).normalize.to_s
-      media_attachment = MediaAttachment.create(status: status, account: status.account, remote_url: href, description: attachment['name'].presence)
+      media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence)
+      media_attachments << media_attachment
 
       next if skip_download?
 
       media_attachment.file_remote_url = href
       media_attachment.save
     end
+
+    media_attachments
   rescue Addressable::URI::InvalidURIError => e
     Rails.logger.debug e
+
+    media_attachments
+  end
+
+  def attach_media(status, media_attachments)
+    return if media_attachments.blank?
+
+    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+    media.update(status_id: status.id)
   end
 
   def resolve_thread(status)
diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb
index 3418e2420..f210e134a 100644
--- a/app/lib/ostatus/activity/creation.rb
+++ b/app/lib/ostatus/activity/creation.rb
@@ -26,6 +26,8 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     cached_reblog = reblog
     status = nil
 
+    media_attachments = save_media
+
     ApplicationRecord.transaction do
       status = Status.create!(
         uri: id,
@@ -44,7 +46,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
 
       save_mentions(status)
       save_hashtags(status)
-      save_media(status)
+      attach_media(status, media_attachments)
       save_emojis(status)
     end
 
@@ -126,18 +128,20 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
     ProcessHashtagsService.new.call(parent, tags)
   end
 
-  def save_media(parent)
-    do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
+  def save_media
+    do_not_download = DomainBlock.find_by(domain: @account.domain)&.reject_media?
+    media_attachments = []
 
     @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
       next unless link['href']
 
-      media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href'])
+      media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href'])
       parsed_url = Addressable::URI.parse(link['href']).normalize
 
       next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty?
 
       media.save
+      media_attachments << media
 
       next if do_not_download
 
@@ -148,6 +152,15 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
         next
       end
     end
+
+    media_attachments
+  end
+
+  def attach_media(parent, media_attachments)
+    return if media_attachments.blank?
+
+    media = MediaAttachment.where(status_id: nil, id: media_attachments.take(4).map(&:id))
+    media.update(status_id: parent.id)
   end
 
   def save_emojis(parent)
diff --git a/app/models/account.rb b/app/models/account.rb
index 48b17bbb8..c75ea028e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -287,6 +287,7 @@ class Account < ApplicationRecord
         FROM accounts
         WHERE #{query} @@ #{textsearch}
           AND accounts.suspended = false
+          AND accounts.moved_to_account_id IS NULL
         ORDER BY rank DESC
         LIMIT ?
       SQL
@@ -312,6 +313,7 @@ class Account < ApplicationRecord
           WHERE accounts.id IN (SELECT * FROM first_degree)
             AND #{query} @@ #{textsearch}
             AND accounts.suspended = false
+            AND accounts.moved_to_account_id IS NULL
           GROUP BY accounts.id
           ORDER BY rank DESC
           LIMIT ?
@@ -327,6 +329,7 @@ class Account < ApplicationRecord
           LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
           WHERE #{query} @@ #{textsearch}
             AND accounts.suspended = false
+            AND accounts.moved_to_account_id IS NULL
           GROUP BY accounts.id
           ORDER BY rank DESC
           LIMIT ?
diff --git a/app/models/list.rb b/app/models/list.rb
index 910864b26..be85c3b87 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -4,7 +4,7 @@
 # Table name: lists
 #
 #  id         :integer          not null, primary key
-#  account_id :integer
+#  account_id :integer          not null
 #  title      :string           default(""), not null
 #  created_at :datetime         not null
 #  updated_at :datetime         not null
@@ -13,6 +13,8 @@
 class List < ApplicationRecord
   include Paginable
 
+  PER_ACCOUNT_LIMIT = 50
+
   belongs_to :account
 
   has_many :list_accounts, inverse_of: :list, dependent: :destroy
@@ -20,6 +22,10 @@ class List < ApplicationRecord
 
   validates :title, presence: true
 
+  validates_each :account_id, on: :create do |record, _attr, value|
+    record.errors.add(:base, I18n.t('lists.errors.limit')) if List.where(account_id: value).count >= PER_ACCOUNT_LIMIT
+  end
+
   before_destroy :clean_feed_manager
 
   private
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 5baddba8a..716b82243 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -33,7 +33,7 @@ class PreviewCard < ApplicationRecord
 
   has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: { original: '280x280>' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, styles: { original: '400x400>' }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
   include Remotable
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 0fa08e157..dc2c8d129 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -23,7 +23,7 @@ class Tag < ApplicationRecord
 
   class << self
     def search_for(term, limit = 5)
-      pattern = sanitize_sql_like(term) + '%'
+      pattern = sanitize_sql_like(term.strip) + '%'
       Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
     end
   end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index a289ceac4..3be110665 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -4,7 +4,7 @@ class AccountSearchService < BaseService
   attr_reader :query, :limit, :options, :account
 
   def call(query, limit, account = nil, options = {})
-    @query   = query
+    @query   = query.strip
     @limit   = limit
     @options = options
     @account = account
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index cec96d927..7f4518ea7 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -38,7 +38,7 @@ class FetchLinkCardService < BaseService
     @card ||= PreviewCard.new(url: @url)
     res     = Request.new(:head, @url).perform
 
-    return if res.code != 200 || res.mime_type != 'text/html'
+    return if res.code != 405 && (res.code != 200 || res.mime_type != 'text/html')
 
     attempt_oembed || attempt_opengraph
   end
diff --git a/app/services/resolve_remote_account_service.rb b/app/services/resolve_remote_account_service.rb
index 3293fe40f..d7d0be210 100644
--- a/app/services/resolve_remote_account_service.rb
+++ b/app/services/resolve_remote_account_service.rb
@@ -44,7 +44,7 @@ class ResolveRemoteAccountService < BaseService
       if lock.acquired?
         @account = Account.find_remote(@username, @domain)
 
-        if activitypub_ready?
+        if activitypub_ready? || @account&.activitypub?
           handle_activitypub
         else
           handle_ostatus
diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb
index 9760e1138..64da04120 100644
--- a/app/validators/status_pin_validator.rb
+++ b/app/validators/status_pin_validator.rb
@@ -2,9 +2,9 @@
 
 class StatusPinValidator < ActiveModel::Validator
   def validate(pin)
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
-    pin.errors.add(:status, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility)
+    pin.errors.add(:base, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count > 4
   end
 end
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index b488bd9ba..d88ec8280 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -17,16 +17,16 @@
       %p{ style: 'margin-bottom: 0' }<
         %span.p-summary> #{Formatter.instance.format_spoiler(status)}&nbsp;
         %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
-    .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
-      = Formatter.instance.format(status, custom_emojify: true)
-      - if !status.media_attachments.empty?
-        - if status.media_attachments.first.video?
-          - video = status.media_attachments.first
-          %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380) }}<
-        - else
-          %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
-      - elsif status.preview_cards.first
-        %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}<
+    .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true)
+
+  - if !status.media_attachments.empty?
+    - if status.media_attachments.first.video?
+      - video = status.media_attachments.first
+      %div{ data: { component: 'Video', props: Oj.dump(src: video.file.url(:original), preview: video.file.url(:small), sensitive: status.sensitive?, width: 670, height: 380, detailed: true) }}<
+    - else
+      %div{ data: { component: 'MediaGallery', props: Oj.dump(height: 380, sensitive: status.sensitive?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }) }}<
+  - elsif status.preview_cards.first
+    %div{ data: { component: 'Card', props: Oj.dump('maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_cards.first, serializer: REST::PreviewCardSerializer).as_json) }}
 
   .detailed-status__meta
     %data.dt-published{ value: status.created_at.to_time.iso8601 }
diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml
index 895a61247..b52334a28 100644
--- a/app/views/stream_entries/show.html.haml
+++ b/app/views/stream_entries/show.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_title do
+  = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.text, length: 50, omission: '…'))
+
 - content_for :header_tags do
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex' }/
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index e05fe1c39..03f19e20a 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -19,8 +19,11 @@
           %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'
+            - if user_signed_in?
+              = link_to t('settings.back'), root_path, class: 'button button-secondary'
+            - else
+              = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary'
+            = link_to t('about.learn_more'), about_path, class: 'button button-alternative'
 
         .features-list
           .features-list__row