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/v1/accounts/featured_tags_controller.rb2
-rw-r--r--app/controllers/settings/pictures_controller.rb8
-rw-r--r--app/javascript/flavours/glitch/components/button.js12
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js12
-rw-r--r--app/javascript/flavours/glitch/features/list_timeline/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js2
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss4
-rw-r--r--app/javascript/flavours/glitch/styles/rtl.scss5
-rw-r--r--app/javascript/flavours/glitch/styles/statuses.scss15
-rw-r--r--app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap49
-rw-r--r--app/javascript/mastodon/components/button.js14
-rw-r--r--app/javascript/mastodon/containers/media_container.js17
-rw-r--r--app/javascript/mastodon/features/account/components/header.js12
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js10
-rw-r--r--app/javascript/mastodon/features/video/index.js4
-rw-r--r--app/javascript/mastodon/locales/hy.json206
-rw-r--r--app/javascript/mastodon/locales/ml.json32
-rw-r--r--app/javascript/mastodon/locales/vi.json8
-rw-r--r--app/javascript/styles/mastodon/components.scss21
-rw-r--r--app/javascript/styles/mastodon/containers.scss4
-rw-r--r--app/javascript/styles/mastodon/rtl.scss5
-rw-r--r--app/javascript/styles/mastodon/statuses.scss15
-rw-r--r--app/models/concerns/account_interactions.rb8
-rw-r--r--app/models/concerns/follow_limitable.rb17
-rw-r--r--app/models/follow.rb2
-rw-r--r--app/models/follow_request.rb2
-rw-r--r--app/serializers/rest/account_featured_tag_serializer.rb15
-rw-r--r--app/serializers/rest/featured_tag_serializer.rb8
-rw-r--r--app/services/batched_remove_status_service.rb2
-rw-r--r--app/services/delete_account_service.rb9
-rw-r--r--app/services/follow_service.rb7
-rw-r--r--app/services/suspend_account_service.rb8
-rw-r--r--app/services/unsuspend_account_service.rb8
-rw-r--r--app/views/admin/reports/_status.html.haml2
-rw-r--r--app/views/media/player.html.haml2
-rw-r--r--app/views/statuses/_detailed_status.html.haml2
-rw-r--r--app/views/statuses/_simple_status.html.haml2
-rw-r--r--app/workers/authorize_follow_worker.rb2
-rw-r--r--app/workers/import/relationship_worker.rb6
-rw-r--r--app/workers/refollow_worker.rb2
-rw-r--r--app/workers/unfollow_follow_worker.rb2
43 files changed, 292 insertions, 281 deletions
diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb
index dc01b577c..0101fb469 100644
--- a/app/controllers/api/v1/accounts/featured_tags_controller.rb
+++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb
@@ -7,7 +7,7 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
   respond_to :json
 
   def index
-    render json: @featured_tags, each_serializer: REST::AccountFeaturedTagSerializer
+    render json: @featured_tags, each_serializer: REST::FeaturedTagSerializer
   end
 
   private
diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb
index 28df65f8f..58a432530 100644
--- a/app/controllers/settings/pictures_controller.rb
+++ b/app/controllers/settings/pictures_controller.rb
@@ -7,8 +7,12 @@ module Settings
 
     def destroy
       if valid_picture?
-        msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
-        redirect_to settings_profile_path, notice: msg, status: 303
+        if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
+          ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
+          redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
+        else
+          redirect_to settings_profile_path
+        end
       else
         bad_request
       end
diff --git a/app/javascript/flavours/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.js
index cd6528f58..b1815c3e1 100644
--- a/app/javascript/flavours/glitch/components/button.js
+++ b/app/javascript/flavours/glitch/components/button.js
@@ -10,17 +10,11 @@ export default class Button extends React.PureComponent {
     disabled: PropTypes.bool,
     block: PropTypes.bool,
     secondary: PropTypes.bool,
-    size: PropTypes.number,
     className: PropTypes.string,
     title: PropTypes.string,
-    style: PropTypes.object,
     children: PropTypes.node,
   };
 
-  static defaultProps = {
-    size: 36,
-  };
-
   handleClick = (e) => {
     if (!this.props.disabled) {
       this.props.onClick(e);
@@ -44,12 +38,6 @@ export default class Button extends React.PureComponent {
       disabled: this.props.disabled,
       onClick: this.handleClick,
       ref: this.setRef,
-      style: {
-        padding: `0 ${this.props.size / 2.25}px`,
-        height: `${this.props.size}px`,
-        lineHeight: `${this.props.size}px`,
-        ...this.props.style,
-      },
     };
 
     if (this.props.title) attrs.title = this.props.title;
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 9b080a14e..15515a99a 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -159,13 +159,17 @@ class Header extends ImmutablePureComponent {
       info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
     }
 
+    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
+      bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
+    }
+
     if (me !== account.get('id')) {
       if (!account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = '';
       } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
+        actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
       } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
+        actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
       }
@@ -173,10 +177,6 @@ class Header extends ImmutablePureComponent {
       actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
     }
 
-    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
-      bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
-    }
-
     if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
       actionBtn = '';
     }
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index d826c8ccd..9e231aab7 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -194,7 +194,7 @@ class ListTimeline extends React.PureComponent {
               </span>
               <div className='column-settings__row'>
                 { ['none', 'list', 'followed'].map(policy => (
-                  <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
+                  <RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
                 ))}
               </div>
             </div>
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 92dcaf473..a81311c67 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -127,7 +127,7 @@ class Video extends React.PureComponent {
   };
 
   static defaultProps = {
-    frameRate: 25,
+    frameRate: '25',
   };
 
   state = {
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 3e67754c5..40206c696 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -141,6 +141,11 @@
     display: block;
     width: 100%;
   }
+
+  .layout-multiple-columns &.button--with-bell {
+    font-size: 12px;
+    padding: 0 8px;
+  }
 }
 
 .icon-button {
@@ -923,6 +928,7 @@
     flex: 0 0 auto;
     padding: 10px;
     padding-top: 20px;
+    z-index: 1;
 
     ul {
       margin-bottom: 10px;
diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index d1c6c33d7..63374f3c3 100644
--- a/app/javascript/flavours/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
@@ -446,6 +446,10 @@
       }
     }
 
+    .logo-button {
+      padding: 3px 15px;
+    }
+
     &__image {
       border-radius: 4px 4px 0 0;
       overflow: hidden;
diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss
index 2375bac90..f6a90d271 100644
--- a/app/javascript/flavours/glitch/styles/rtl.scss
+++ b/app/javascript/flavours/glitch/styles/rtl.scss
@@ -163,6 +163,11 @@ body.rtl {
     right: 42px;
   }
 
+  .account__header__tabs__buttons > .icon-button {
+    margin-right: 0;
+    margin-left: 8px;
+  }
+
   .account__avatar-overlay-overlay {
     right: auto;
     left: 0;
diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss
index 58f74f954..b807fa45a 100644
--- a/app/javascript/flavours/glitch/styles/statuses.scss
+++ b/app/javascript/flavours/glitch/styles/statuses.scss
@@ -79,9 +79,14 @@
   background: $ui-highlight-color;
   color: $primary-text-color;
   text-transform: none;
-  line-height: 36px;
+  line-height: 1.2;
   height: auto;
-  padding: 3px 15px;
+  min-height: 36px;
+  min-width: 88px;
+  white-space: normal;
+  overflow-wrap: break-word;
+  hyphens: auto;
+  padding: 0 15px;
   border: 0;
 
   svg {
@@ -122,6 +127,12 @@
   }
 }
 
+a.button.logo-button {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
 .embed,
 .public-layout {
   .status__content[data-spoiler=folded] {
diff --git a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
index 5c04e0979..86fbba917 100644
--- a/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
+++ b/app/javascript/mastodon/components/__tests__/__snapshots__/button-test.js.snap
@@ -4,13 +4,6 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
 <button
   className="button button-secondary"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 />
 `;
 
@@ -18,13 +11,6 @@ exports[`<Button /> renders a button element 1`] = `
 <button
   className="button"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 />
 `;
 
@@ -33,13 +19,6 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
   className="button"
   disabled={true}
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 />
 `;
 
@@ -47,13 +26,6 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
 <button
   className="button button--block"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 />
 `;
 
@@ -61,13 +33,6 @@ exports[`<Button /> renders the children 1`] = `
 <button
   className="button"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 >
   <p>
     children
@@ -79,13 +44,6 @@ exports[`<Button /> renders the given text 1`] = `
 <button
   className="button"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 >
   foo
 </button>
@@ -95,13 +53,6 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
 <button
   className="button"
   onClick={[Function]}
-  style={
-    Object {
-      "height": "36px",
-      "lineHeight": "36px",
-      "padding": "0 16px",
-    }
-  }
 >
   foo
 </button>
diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js
index eb8dd7dc8..85b2d78ca 100644
--- a/app/javascript/mastodon/components/button.js
+++ b/app/javascript/mastodon/components/button.js
@@ -10,17 +10,11 @@ export default class Button extends React.PureComponent {
     disabled: PropTypes.bool,
     block: PropTypes.bool,
     secondary: PropTypes.bool,
-    size: PropTypes.number,
     className: PropTypes.string,
     title: PropTypes.string,
-    style: PropTypes.object,
     children: PropTypes.node,
   };
 
-  static defaultProps = {
-    size: 36,
-  };
-
   handleClick = (e) => {
     if (!this.props.disabled) {
       this.props.onClick(e);
@@ -36,13 +30,6 @@ export default class Button extends React.PureComponent {
   }
 
   render () {
-    const style = {
-      padding: `0 ${this.props.size / 2.25}px`,
-      height: `${this.props.size}px`,
-      lineHeight: `${this.props.size}px`,
-      ...this.props.style,
-    };
-
     const className = classNames('button', this.props.className, {
       'button-secondary': this.props.secondary,
       'button--block': this.props.block,
@@ -54,7 +41,6 @@ export default class Button extends React.PureComponent {
         disabled={this.props.disabled}
         onClick={this.handleClick}
         ref={this.setRef}
-        style={style}
         title={this.props.title}
       >
         {this.props.text || this.props.children}
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index afed6868e..52fdc9294 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -2,7 +2,7 @@ import React, { PureComponent, Fragment } from 'react';
 import ReactDOM from 'react-dom';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
-import { List as ImmutableList, fromJS } from 'immutable';
+import { fromJS } from 'immutable';
 import { getLocale } from 'mastodon/locales';
 import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
 import MediaGallery from 'mastodon/components/media_gallery';
@@ -31,6 +31,7 @@ export default class MediaContainer extends PureComponent {
     index: null,
     time: null,
     backgroundColor: null,
+    options: null,
   };
 
   handleOpenMedia = (media, index) => {
@@ -40,13 +41,15 @@ export default class MediaContainer extends PureComponent {
     this.setState({ media, index });
   }
 
-  handleOpenVideo = (video, time) => {
-    const media = ImmutableList([video]);
+  handleOpenVideo = (options) => {
+    const { components } = this.props;
+    const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
+    const mediaList = fromJS(media);
 
     document.body.classList.add('with-modals--active');
     document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
 
-    this.setState({ media, time });
+    this.setState({ media: mediaList, options });
   }
 
   handleCloseMedia = () => {
@@ -58,6 +61,7 @@ export default class MediaContainer extends PureComponent {
       index: null,
       time: null,
       backgroundColor: null,
+      options: null,
     });
   }
 
@@ -83,6 +87,7 @@ export default class MediaContainer extends PureComponent {
               ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
 
               ...(componentName === 'Video' ? {
+                componetIndex: i,
                 onOpenVideo: this.handleOpenVideo,
               } : {
                 onOpenMedia: this.handleOpenMedia,
@@ -100,7 +105,9 @@ export default class MediaContainer extends PureComponent {
               <MediaModal
                 media={this.state.media}
                 index={this.state.index || 0}
-                time={this.state.time}
+                currentTime={this.state.options?.startTime}
+                autoPlay={this.state.options?.autoPlay}
+                volume={this.state.options?.defaultVolume}
                 onClose={this.handleCloseMedia}
                 onChangeBackgroundColor={this.setBackgroundColor}
               />
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 2b97af4e6..b47ebed62 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -164,13 +164,17 @@ class Header extends ImmutablePureComponent {
       info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
     }
 
+    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
+      bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
+    }
+
     if (me !== account.get('id')) {
       if (!account.get('relationship')) { // Wait until the relationship is loaded
         actionBtn = '';
       } else if (account.getIn(['relationship', 'requested'])) {
-        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
+        actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
       } else if (!account.getIn(['relationship', 'blocking'])) {
-        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
+        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
       } else if (account.getIn(['relationship', 'blocking'])) {
         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
       }
@@ -178,10 +182,6 @@ class Header extends ImmutablePureComponent {
       actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
     }
 
-    if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
-      bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
-    }
-
     if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
       actionBtn = '';
     }
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 02b018247..8eb645630 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -194,7 +194,7 @@ class ListTimeline extends React.PureComponent {
               </span>
               <div className='column-settings__row'>
                 { ['none', 'list', 'followed'].map(policy => (
-                  <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
+                  <RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
                 ))}
               </div>
             </div>
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 7fe7ed094..08da10330 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -32,6 +32,9 @@ class MediaModal extends ImmutablePureComponent {
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     onChangeBackgroundColor: PropTypes.func.isRequired,
+    currentTime: PropTypes.number,
+    autoPlay: PropTypes.bool,
+    volume: PropTypes.number,
   };
 
   static contextTypes = {
@@ -183,7 +186,7 @@ class MediaModal extends ImmutablePureComponent {
           />
         );
       } else if (image.get('type') === 'video') {
-        const { time } = this.props;
+        const { currentTime, autoPlay, volume } = this.props;
 
         return (
           <Video
@@ -192,7 +195,10 @@ class MediaModal extends ImmutablePureComponent {
             src={image.get('url')}
             width={image.get('width')}
             height={image.get('height')}
-            currentTime={time || 0}
+            frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
+            currentTime={currentTime || 0}
+            autoPlay={autoPlay || false}
+            volume={volume || 1}
             onCloseVideo={onClose}
             detailed
             alt={image.get('description')}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index a2dccdfc0..70e3cd6e8 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -121,10 +121,11 @@ class Video extends React.PureComponent {
     autoPlay: PropTypes.bool,
     volume: PropTypes.number,
     muted: PropTypes.bool,
+    componetIndex: PropTypes.number,
   };
 
   static defaultProps = {
-    frameRate: 25,
+    frameRate: '25',
   };
 
   state = {
@@ -501,6 +502,7 @@ class Video extends React.PureComponent {
       startTime: this.video.currentTime,
       autoPlay: !this.state.paused,
       defaultVolume: this.state.volume,
+      componetIndex: this.props.componetIndex,
     });
   }
 
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 4d9d0b63e..5cdba58e8 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -17,8 +17,8 @@
   "account.follow": "Հետեւել",
   "account.followers": "Հետեւողներ",
   "account.followers.empty": "Այս օգտատիրոջը դեռ ոչ մէկ չի հետեւում։",
-  "account.followers_counter": "{count, plural, one {{counter} Հետևորդ} other {{counter} Հետևորդներ}}",
-  "account.following_counter": "{count, plural, other {{counter} Հետևում են}}",
+  "account.followers_counter": "{count, plural, one {{counter} Հետեւորդ} other {{counter} Հետեւորդներ}}",
+  "account.following_counter": "{count, plural, other {{counter} Հետեւում են}}",
   "account.follows.empty": "Այս օգտատէրը դեռ ոչ մէկի չի հետեւում։",
   "account.follows_you": "Հետեւում է քեզ",
   "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները",
@@ -45,7 +45,7 @@
   "account.unfollow": "Ապահետեւել",
   "account.unmute": "Ապալռեցնել @{name}֊ին",
   "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
-  "account_note.placeholder": "Սեղմեք գրառելու համար",
+  "account_note.placeholder": "Սեղմէ՛ք գրառելու համար\n",
   "alert.rate_limited.message": "Փորձէք  որոշ ժամանակ անց՝ {retry_time, time, medium}։",
   "alert.rate_limited.title": "Գործողութիւնների յաճախութիւնը գերազանցում է թոյլատրելին",
   "alert.unexpected.message": "Անսպասելի սխալ տեղի ունեցաւ։",
@@ -57,9 +57,9 @@
   "bundle_column_error.retry": "Կրկին փորձել",
   "bundle_column_error.title": "Ցանցային սխալ",
   "bundle_modal_error.close": "Փակել",
-  "bundle_modal_error.message": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանվեց։",
+  "bundle_modal_error.message": "Այս բաղադրիչը բեռնելու ընթացքում ինչ֊որ բան խափանուեց։",
   "bundle_modal_error.retry": "Կրկին փորձել",
-  "column.blocks": "Արգելափակված օգտատերեր",
+  "column.blocks": "Արգելափակուած օգտատէրեր",
   "column.bookmarks": "Էջանիշեր",
   "column.community": "Տեղական հոսք",
   "column.direct": "Հասցէագրուած հաղորդագրութիւններ",
@@ -69,27 +69,27 @@
   "column.follow_requests": "Հետեւելու հայցեր",
   "column.home": "Հիմնական",
   "column.lists": "Ցանկեր",
-  "column.mutes": "Լռեցրած օգտատերեր",
+  "column.mutes": "Լռեցրած օգտատէրեր",
   "column.notifications": "Ծանուցումներ",
-  "column.pins": "Ամրացված թթեր",
+  "column.pins": "Ամրացուած թթեր",
   "column.public": "Դաշնային հոսք",
   "column_back_button.label": "Ետ",
-  "column_header.hide_settings": "Թաքցնել կարգավորումները",
+  "column_header.hide_settings": "Թաքցնել կարգաւորումները",
   "column_header.moveLeft_settings": "Տեղաշարժել սիւնը ձախ",
   "column_header.moveRight_settings": "Տեղաշարժել սիւնը աջ",
   "column_header.pin": "Ամրացնել",
-  "column_header.show_settings": "Ցուցադրել կարգավորումները",
+  "column_header.show_settings": "Ցուցադրել կարգաւորումները",
   "column_header.unpin": "Հանել",
-  "column_subheading.settings": "Կարգավորումներ",
-  "community.column_settings.local_only": "Միայն ներքին",
-  "community.column_settings.media_only": "Media only",
-  "community.column_settings.remote_only": "Միայն հեռակա",
-  "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
+  "column_subheading.settings": "Կարգաւորումներ",
+  "community.column_settings.local_only": "Միայն տեղական",
+  "community.column_settings.media_only": "Միայն մեդիա",
+  "community.column_settings.remote_only": "Միայն հեռակայ",
+  "compose_form.direct_message_warning": "Այս թութը տեսանելի կը լինի միայն նշուած օգտատէրերին։",
   "compose_form.direct_message_warning_learn_more": "Իմանալ աւելին",
-  "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։",
+  "compose_form.hashtag_warning": "Այս թութը չի հաշուառուի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարաւոր է որոնել պիտակներով։",
   "compose_form.lock_disclaimer": "Քո հաշիւը {locked} չէ։ Իւրաքանչիւրութիւն ոք կարող է հետեւել քեզ եւ տեսնել միայն հետեւողների համար նախատեսուած գրառումները։",
   "compose_form.lock_disclaimer.lock": "փակ",
-  "compose_form.placeholder": "Ի՞նչ կա մտքիդ",
+  "compose_form.placeholder": "Ի՞նչ կայ մտքիդ",
   "compose_form.poll.add_option": "Աւելացնել տարբերակ",
   "compose_form.poll.duration": "Հարցման տեւողութիւնը",
   "compose_form.poll.option_placeholder": "Տարբերակ {number}",
@@ -111,16 +111,16 @@
   "confirmations.delete.confirm": "Ջնջել",
   "confirmations.delete.message": "Վստա՞հ ես, որ ուզում ես ջնջել այս թութը։",
   "confirmations.delete_list.confirm": "Ջնջել",
-  "confirmations.delete_list.message": "Վստա՞հ ես, որ ուզում ես մշտապես ջնջել այս ցանկը։",
+  "confirmations.delete_list.message": "Վստա՞հ ես, որ ուզում ես մշտապէս ջնջել այս ցանկը։",
   "confirmations.domain_block.confirm": "Թաքցնել ամբողջ տիրույթը",
-  "confirmations.domain_block.message": "Հաստատ֊հաստա՞տ վստահ ես, որ ուզում ես արգելափակել ամբողջ {domain} տիրույթը։ Սովորաբար մի երկու թիրախավորված արգելափակում կամ լռեցում բավական է ու նախընտրելի։",
+  "confirmations.domain_block.message": "Հաստատ֊հաստա՞տ վստահ ես, որ ուզում ես արգելափակել ամբողջ {domain} տիրոյթը։ Սովորաբար մի երկու թիրախաւորուած արգելափակում կամ լռեցում բաւական է ու նախընտրելի։",
   "confirmations.logout.confirm": "Ելք",
   "confirmations.logout.message": "Համոզո՞ւած ես, որ ուզում ես դուրս գալ",
   "confirmations.mute.confirm": "Լռեցնել",
   "confirmations.mute.explanation": "Սա թաքցնելու ա իրենց գրառումներն, ինչպէս նաեւ իրենց նշող գրառումներն, բայց իրենք միեւնոյն է կը կարողանան հետեւել ձեզ եւ տեսնել ձեր գրառումները։",
   "confirmations.mute.message": "Վստա՞հ ես, որ ուզում ես {name}֊ին լռեցնել։",
   "confirmations.redraft.confirm": "Ջնջել եւ խմբագրել նորից",
-  "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.",
+  "confirmations.redraft.message": "Վստահ ե՞ս, որ ցանկանում ես ջնջել եւ վերախմբագրել այս թութը։ Դու կը կորցնես այս գրառման բոլոր պատասխանները, տարածումները եւ հաւանումները։",
   "confirmations.reply.confirm": "Պատասխանել",
   "confirmations.reply.message": "Այս պահին պատասխանելը կը չեղարկի ձեր՝ այս պահին անաւարտ հաղորդագրութիւնը։ Համոզուա՞ծ էք։",
   "confirmations.unfollow.confirm": "Ապահետեւել",
@@ -134,67 +134,67 @@
   "directory.new_arrivals": "Նորեկներ",
   "directory.recently_active": "Վերջերս ակտիւ",
   "embed.instructions": "Այս թութը քո կայքում ներդնելու համար կարող ես պատճէնել ներքինանալ կոդը։",
-  "embed.preview": "Ահա, թե ինչ տեսք կունենա այն՝",
+  "embed.preview": "Ահայ, թէ ինչ տեսք կը ունենայ այն՝",
   "emoji_button.activity": "Զբաղմունքներ",
-  "emoji_button.custom": "Հատուկ",
-  "emoji_button.flags": "Դրոշներ",
+  "emoji_button.custom": "Յատուկ",
+  "emoji_button.flags": "Դրօշներ",
   "emoji_button.food": "Կերուխում",
-  "emoji_button.label": "Էմոջի ավելացնել",
+  "emoji_button.label": "Էմոջի աւելացնել",
   "emoji_button.nature": "Բնութիւն",
-  "emoji_button.not_found": "Նման էմոջիներ դեռ չեն հայտնաբերվել։ (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Նման էմոջիներ դեռ չեն յայտնաբերուել։ (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Առարկաներ",
   "emoji_button.people": "Մարդիկ",
-  "emoji_button.recent": "Հաճախ օգտագործվող",
+  "emoji_button.recent": "Յաճախ օգտագործուող",
   "emoji_button.search": "Որոնել…",
   "emoji_button.search_results": "Որոնման արդիւնքներ",
   "emoji_button.symbols": "Նշաններ",
   "emoji_button.travel": "Ուղեւորութիւն եւ տեղանքներ",
-  "empty_column.account_suspended": "Account suspended",
+  "empty_column.account_suspended": "Հաշիւը արգելափակուած է",
   "empty_column.account_timeline": "Այստեղ թթեր չկա՛ն։",
   "empty_column.account_unavailable": "Անձնական էջը հասանելի չի",
   "empty_column.blocks": "Դու դեռ ոչ մէկի չես արգելափակել։",
-  "empty_column.bookmarked_statuses": "Դու դեռ չունես որեւէ էջանշւած թութ։ Երբ էջանշես, դրանք կերեւան այստեղ։",
-  "empty_column.community": "Տեղական հոսքը դատա՛րկ է։ Հրապարակային մի բան գրիր շարժիչը խոդ տալու համար։",
+  "empty_column.bookmarked_statuses": "Դու դեռ չունես որեւէ էջանշւած թութ։ Երբ էջանշես, դրանք կը երեւան այստեղ։",
+  "empty_column.community": "Տեղական հոսքը դատարկ է։ Հրապարակային մի բան գրի՛ր շարժիչը գործարկելու համար։",
   "empty_column.direct": "Դու դեռ չունես ոչ մի հասցէագրուած հաղորդագրութիւն։ Երբ ուղարկես կամ ստանաս որեւէ անձնական նամակ, այն այստեղ կերեւայ։",
   "empty_column.domain_blocks": "Թաքցուած տիրոյթներ դեռ չկան։",
   "empty_column.favourited_statuses": "Դու դեռ չունես որեւէ հաւանած թութ։ Երբ հաւանես, դրանք կերեւան այստեղ։",
   "empty_column.favourites": "Այս թութը ոչ մէկ դեռ չի հաւանել։ Հաւանողները կերեւան այստեղ, երբ նշեն թութը հաւանած։",
   "empty_column.follow_requests": "Դու դեռ չունես որեւէ հետեւելու յայտ։ Բոլոր նման յայտերը կը յայտնուեն այստեղ։",
-  "empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկա։",
-  "empty_column.home": "Քո հիմնական հոսքը դատա՛րկ է։ Այցելի՛ր {public}ը կամ օգտվիր որոնումից՝ այլ մարդկանց հանդիպելու համար։",
+  "empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկայ։",
+  "empty_column.home": "Քո հիմնական հոսքը դատարկ է։ Այցելի՛ր {public}ը կամ օգտուիր որոնումից՝ այլ մարդկանց հանդիպելու համար։",
   "empty_column.home.public_timeline": "հրապարակային հոսք",
-  "empty_column.list": "Այս ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կհայտնվի այստեղ։",
-  "empty_column.lists": "Դուք դեռ չունեք ստեղծած ցանկ։ Ցանկ ստեղծելուն պես այն կհայտնվի այստեղ։",
-  "empty_column.mutes": "Առայժմ ոչ ոքի չեք լռեցրել։",
+  "empty_column.list": "Այս ցանկում դեռ ոչինչ չկայ։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կը յայտնուի այստեղ։",
+  "empty_column.lists": "Դուք դեռ չունէք ստեղծած ցանկ։ Ցանկ ստեղծելուն պէս այն կը յայտնուի այստեղ։",
+  "empty_column.mutes": "Առայժմ ոչ ոքի չէք լռեցրել։",
   "empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր միւսներին՝ խօսակցութիւնը սկսելու համար։",
   "empty_column.public": "Այստեղ բան չկա՛յ։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգոյցներից էակների՝ այն լցնելու համար։",
   "error.unexpected_crash.explanation": "Մեր ծրագրակազմում վրիպակի կամ դիտարկչի անհամատեղելիութեան պատճառով այս էջը չի կարող լիարժէք պատկերուել։",
   "error.unexpected_crash.explanation_addons": "Այս էջի ճիշտ պատկերումը չի ստացում։ Խափանումը հաւանաբար առաջացել է դիտարկիչի յավելվածից կամ առցանց թարգմանիչից։",
-  "error.unexpected_crash.next_steps": "Փորձիր թարմացնել էջը։ Եթե դա չօգնի ապա կարող ես օգտվել Մաստադոնից ուրիշ դիտարկիչով կամ հավելվածով։",
+  "error.unexpected_crash.next_steps": "Փորձիր թարմացնել էջը։ Եթէ դա չօգնի ապա կարող ես օգտուել Մաստադոնից ուրիշ դիտարկիչով կամ յաւելուածով։",
   "error.unexpected_crash.next_steps_addons": "Փորձիր անջատել յաւելուածները եւ թարմացնել էջը։ Եթե դա չօգնի, կարող ես օգտուել Մաստադոնից այլ դիտարկիչով կամ յաւելուածով։",
   "errors.unexpected_crash.copy_stacktrace": "Պատճենել սթաքթրեյսը սեղմատախտակին",
   "errors.unexpected_crash.report_issue": "Զեկուցել խնդրի մասին",
-  "follow_request.authorize": "Վավերացնել",
+  "follow_request.authorize": "Վաւերացնել",
   "follow_request.reject": "Մերժել",
-  "follow_requests.unlocked_explanation": "Այս հարցումը ուղարկված է հաշվից, որի համար {domain}-ի անձնակազմը միացրել է ձեռքով ստուգում։",
-  "generic.saved": "Պահպանված է",
+  "follow_requests.unlocked_explanation": "Այս հարցումը ուղարկուած է հաշուից, որի համար {domain}-ի անձնակազմը միացրել է ձեռքով ստուգում։",
+  "generic.saved": "Պահպանուած է",
   "getting_started.developers": "Մշակողներ",
   "getting_started.directory": "Օգտատէրերի շտեմարան",
   "getting_started.documentation": "Փաստաթղթեր",
-  "getting_started.heading": "Ինչպես սկսել",
+  "getting_started.heading": "Ինչպէս սկսել",
   "getting_started.invite": "Հրաւիրել մարդկանց",
-  "getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրեպներ զեկուցել ԳիթՀաբում՝ {github}։",
+  "getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրէպներ զեկուցել ԳիթՀաբում՝ {github}։",
   "getting_started.security": "Հաշուի կարգաւորումներ",
   "getting_started.terms": "Ծառայութեան պայմանները",
   "hashtag.column_header.tag_mode.all": "եւ {additional}",
   "hashtag.column_header.tag_mode.any": "կամ {additional}",
   "hashtag.column_header.tag_mode.none": "առանց {additional}",
   "hashtag.column_settings.select.no_options_message": "Առաջարկներ չկան",
-  "hashtag.column_settings.select.placeholder": "Ավելացրու հեշթեգեր…",
+  "hashtag.column_settings.select.placeholder": "Աւելացրու պիտկներ…",
   "hashtag.column_settings.tag_mode.all": "Բոլորը",
   "hashtag.column_settings.tag_mode.any": "Ցանկացածը",
   "hashtag.column_settings.tag_mode.none": "Ոչ մեկը",
-  "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
+  "hashtag.column_settings.tag_toggle": "Ներառել լրացուցիչ պիտակները այս սիւնակում ",
   "home.column_settings.basic": "Հիմնական",
   "home.column_settings.show_reblogs": "Ցուցադրել տարածածները",
   "home.column_settings.show_replies": "Ցուցադրել պատասխանները",
@@ -202,17 +202,17 @@
   "home.show_announcements": "Ցուցադրել յայտարարութիւնները",
   "intervals.full.days": "{number, plural, one {# օր} other {# օր}}",
   "intervals.full.hours": "{number, plural, one {# ժամ} other {# ժամ}}",
-  "intervals.full.minutes": "{number, plural, one {# րոպե} other {# րոպե}}",
-  "introduction.federation.action": "Հաջորդ",
+  "intervals.full.minutes": "{number, plural, one {# րոպէ} other {# րոպէ}}",
+  "introduction.federation.action": "Յաջորդ",
   "introduction.federation.federated.headline": "Դաշնային",
   "introduction.federation.federated.text": "Դաշնեզերքի հարեւան հանգոյցների հանրային գրառումները կը յայտնուեն դաշնային հոսքում։",
   "introduction.federation.home.headline": "Հիմնական",
   "introduction.federation.home.text": "Այն անձանց թթերը ում հետևում ես, կը յայտնուեն հիմնական հոսքում։ Դու կարող ես հետեւել ցանկացած անձի ցանկացած հանգոյցից։",
   "introduction.federation.local.headline": "Տեղային",
   "introduction.federation.local.text": "Տեղական հոսքում կարող ես տեսնել քո հանգոյցի բոլոր հանրային գրառումները։",
-  "introduction.interactions.action": "Finish toot-orial!",
+  "introduction.interactions.action": "Աւարտել թթի դասընթացը",
   "introduction.interactions.favourite.headline": "Նախընտրելի",
-  "introduction.interactions.favourite.text": "Փոխանցիր հեղինակին որ քեզ դուր է եկել իր թութը հավանելով այն։",
+  "introduction.interactions.favourite.text": "Փոխանցիր հեղինակին որ քեզ դուր է եկել իր թութը հաւանելով այն։",
   "introduction.interactions.reblog.headline": "Տարածել",
   "introduction.interactions.reblog.text": "Կիսիր այլ օգտատէրերի թութերը քո հետեւողների հետ տարածելով դրանք քո անձնական էջում։",
   "introduction.interactions.reply.headline": "Պատասխանել",
@@ -220,25 +220,25 @@
   "introduction.welcome.action": "Գնացի՜նք։",
   "introduction.welcome.headline": "Առաջին քայլեր",
   "introduction.welcome.text": "Դաշնեզերքը ողջունում է ձեզ։ Շուտով կը կարողանաս ուղարկել նամակներ ու շփուել տարբեր հանգոյցների ընկերներիդ հետ։ Բայց մտապահիր {domain} հանգոյցը, այն իւրայատուկ է, այստեղ է պահւում քո հաշիւը։",
-  "keyboard_shortcuts.back": "ետ նավարկելու համար",
-  "keyboard_shortcuts.blocked": "արգելափակված օգտատերերի ցանկը բացելու համար",
+  "keyboard_shortcuts.back": "ետ նաւարկելու համար",
+  "keyboard_shortcuts.blocked": "արգելափակուած օգտատէրերի ցանկը բացելու համար",
   "keyboard_shortcuts.boost": "տարածելու համար",
   "keyboard_shortcuts.column": "սիւներից մէկի վրայ սեւեռուելու համար",
-  "keyboard_shortcuts.compose": "շարադրման տիրույթին սեւեռվելու համար",
+  "keyboard_shortcuts.compose": "շարադրման տիրոյթին սեւեռուելու համար",
   "keyboard_shortcuts.description": "Նկարագրութիւն",
-  "keyboard_shortcuts.direct": "հասցեագրված գրվածքների հոսքը բացելու համար",
-  "keyboard_shortcuts.down": "ցանկով ներքեւ շարժվելու համար",
+  "keyboard_shortcuts.direct": "հասցէագրուած գրուածքների հոսքը բացելու համար",
+  "keyboard_shortcuts.down": "ցանկով ներքեւ շարժուելու համար",
   "keyboard_shortcuts.enter": "թութը բացելու համար",
-  "keyboard_shortcuts.favourite": "հավանելու համար",
+  "keyboard_shortcuts.favourite": "հաւանելու համար",
   "keyboard_shortcuts.favourites": "էջանիշերի ցուցակը բացելու համար",
   "keyboard_shortcuts.federated": "դաշնային հոսքին անցնելու համար",
   "keyboard_shortcuts.heading": "Ստեղնաշարի կարճատներ",
   "keyboard_shortcuts.home": "անձնական հոսքին անցնելու համար",
-  "keyboard_shortcuts.hotkey": "Հատուկ ստեղն",
+  "keyboard_shortcuts.hotkey": "Յատուկ ստեղն",
   "keyboard_shortcuts.legend": "այս ձեռնարկը ցուցադրելու համար",
   "keyboard_shortcuts.local": "տեղական հոսքին անցնելու համար",
   "keyboard_shortcuts.mention": "հեղինակին նշելու համար",
-  "keyboard_shortcuts.muted": "լռեցված օգտատերերի ցանկը բացելու համար",
+  "keyboard_shortcuts.muted": "լռեցուած օգտատէրերի ցանկը բացելու համար",
   "keyboard_shortcuts.my_profile": "սեփական էջին անցնելու համար",
   "keyboard_shortcuts.notifications": "ծանուցումների սիւնակը բացելու համար",
   "keyboard_shortcuts.open_media": "ցուցադրել մեդիան",
@@ -247,41 +247,41 @@
   "keyboard_shortcuts.reply": "պատասխանելու համար",
   "keyboard_shortcuts.requests": "հետեւելու հայցերի ցանկը դիտելու համար",
   "keyboard_shortcuts.search": "որոնման դաշտին սեւեռվելու համար",
-  "keyboard_shortcuts.spoilers": "որպեսզի ցուցադրվի/թաքցվի CW դաշտը",
+  "keyboard_shortcuts.spoilers": "որպէսզի ցուցադրուի/թաքցուի CW դաշտը",
   "keyboard_shortcuts.start": "«սկսել» սիւնակը բացելու համար",
   "keyboard_shortcuts.toggle_hidden": "CW֊ի ետեւի տեքստը ցուցադրել֊թաքցնելու համար",
   "keyboard_shortcuts.toggle_sensitivity": "մեդիան ցուցադրել֊թաքցնելու համար",
   "keyboard_shortcuts.toot": "թարմ թութ սկսելու համար",
-  "keyboard_shortcuts.unfocus": "տեքստի/որոնման տիրույթից ապասեւեռվելու համար",
-  "keyboard_shortcuts.up": "ցանկով վերեւ շարժվելու համար",
+  "keyboard_shortcuts.unfocus": "տեքստի/որոնման տիրոյթից ապասեւեռուելու համար",
+  "keyboard_shortcuts.up": "ցանկով վերեւ շարժուելու համար",
   "lightbox.close": "Փակել",
-  "lightbox.compress": "Compress image view box",
-  "lightbox.expand": "Expand image view box",
-  "lightbox.next": "Հաջորդ",
+  "lightbox.compress": "Փակել պատկերի դիտման պատուհանը",
+  "lightbox.expand": "Բացել պատկերի դիտման պատուհանը",
+  "lightbox.next": "Յաջորդ",
   "lightbox.previous": "Նախորդ",
-  "lists.account.add": "Ավելացնել ցանկին",
+  "lists.account.add": "Աւելացնել ցանկին",
   "lists.account.remove": "Հանել ցանկից",
   "lists.delete": "Ջնջել ցանկը",
   "lists.edit": "Փոփոխել ցանկը",
   "lists.edit.submit": "Փոխել վերնագիրը",
-  "lists.new.create": "Ավելացնել ցանկ",
+  "lists.new.create": "Աւելացնել ցանկ",
   "lists.new.title_placeholder": "Նոր ցանկի վերնագիր",
-  "lists.replies_policy.followed": "Any followed user",
-  "lists.replies_policy.list": "Members of the list",
+  "lists.replies_policy.followed": "Ցանկացած հետեւող օգտատէր",
+  "lists.replies_policy.list": "Ցանկի անդամներ",
   "lists.replies_policy.none": "Ոչ ոք",
-  "lists.replies_policy.title": "Show replies to:",
-  "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ",
+  "lists.replies_policy.title": "Ցուցադրել պատասխանները՝",
+  "lists.search": "Փնտրել քո հետեւած մարդկանց մէջ",
   "lists.subheading": "Քո ցանկերը",
   "load_pending": "{count, plural, one {# նոր նիւթ} other {# նոր նիւթ}}",
-  "loading_indicator.label": "Բեռնվում է…",
+  "loading_indicator.label": "Բեռնւում է…",
   "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
-  "missing_indicator.label": "Չգտնվեց",
+  "missing_indicator.label": "Չգտնուեց",
   "missing_indicator.sublabel": "Պաշարը չի գտնւում",
-  "mute_modal.duration": "Տևողություն",
+  "mute_modal.duration": "Տեւողութիւն",
   "mute_modal.hide_notifications": "Թաքցնե՞լ ցանուցումներն այս օգտատիրոջից։",
-  "mute_modal.indefinite": "Անժամկետ",
+  "mute_modal.indefinite": "Անժամկէտ",
   "navigation_bar.apps": "Դիւրակիր յաւելուածներ",
-  "navigation_bar.blocks": "Արգելափակված օգտատերեր",
+  "navigation_bar.blocks": "Արգելափակուած օգտատէրեր",
   "navigation_bar.bookmarks": "Էջանիշեր",
   "navigation_bar.community_timeline": "Տեղական հոսք",
   "navigation_bar.compose": "Գրել նոր թութ",
@@ -297,13 +297,13 @@
   "navigation_bar.keyboard_shortcuts": "Ստեղնաշարի կարճատներ",
   "navigation_bar.lists": "Ցանկեր",
   "navigation_bar.logout": "Դուրս գալ",
-  "navigation_bar.mutes": "Լռեցրած օգտատերեր",
+  "navigation_bar.mutes": "Լռեցրած օգտատէրեր",
   "navigation_bar.personal": "Անձնական",
-  "navigation_bar.pins": "Ամրացված թթեր",
+  "navigation_bar.pins": "Ամրացուած թթեր",
   "navigation_bar.preferences": "Նախապատուութիւններ",
   "navigation_bar.public_timeline": "Դաշնային հոսք",
   "navigation_bar.security": "Անվտանգութիւն",
-  "notification.favourite": "{name} հավանեց թութդ",
+  "notification.favourite": "{name} հաւանեց թութդ",
   "notification.follow": "{name} սկսեց հետեւել քեզ",
   "notification.follow_request": "{name} քեզ հետեւելու հայց է ուղարկել",
   "notification.mention": "{name} նշեց քեզ",
@@ -333,17 +333,17 @@
   "notifications.filter.follows": "Հետեւածները",
   "notifications.filter.mentions": "Նշումները",
   "notifications.filter.polls": "Հարցման արդիւնքները",
-  "notifications.filter.statuses": "Updates from people you follow",
+  "notifications.filter.statuses": "Թարմացումներ հետեւորդներից",
   "notifications.grant_permission": "Թոյլատրել։",
   "notifications.group": "{count} ծանուցում",
   "notifications.mark_as_read": "Համարել բոլոր ծանուցումները ընթերցած",
-  "notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
-  "notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
-  "notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
+  "notifications.permission_denied": "Աշխատատիրոյթի ծանուցումներն անհասանելի են՝ դիտարկչի նախկինում մերժուած թոյլտուութիւնների հայցման հետեւանքով",
+  "notifications.permission_denied_alert": "Աշխատատիրոյթի ծանուցումները չեն կարող միացուել, քանի որ դիտարկչի թոյլտուութիւնները նախկինում մերժուել են",
+  "notifications.permission_required": "Աշխատատիրոյթի ծանուցումներն անհասանելի են, քանի որ անհրաժեշտ թոյլտուութիւնները բացակայում են։",
   "notifications_permission_banner.enable": "Միացնել դիտարկչից ծանուցումները",
-  "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
-  "notifications_permission_banner.title": "Never miss a thing",
-  "picture_in_picture.restore": "Հետ բերել",
+  "notifications_permission_banner.how_to_control": "Ծանուցումներ ստանալու համար, երբ Մաստոդոնը բաց չէ՝ ակտիւացրու աշխատատիրոյթի ծանուցումները։ Դու կարող ես ճշգրտօրէն վերահսկել թէ ինչպիսի փոխգործակցութիւններ առաջանան աշխատատիրոյթի ծանուցումներից՝ {icon}ի կոճակով՝ այն ակտիւացնելուց յետոյ։",
+  "notifications_permission_banner.title": "Ոչինչ բաց մի թող",
+  "picture_in_picture.restore": "Յետ բերել",
   "poll.closed": "Փակ",
   "poll.refresh": "Թարմացնել",
   "poll.total_people": "{count, plural, one {# հոգի} other {# հոգի}}",
@@ -353,8 +353,8 @@
   "poll_button.add_poll": "Աւելացնել հարցում",
   "poll_button.remove_poll": "Հեռացնել հարցումը",
   "privacy.change": "Կարգաւորել թթի գաղտնիութիւնը",
-  "privacy.direct.long": "Թթել միայն նշված օգտատերերի համար",
-  "privacy.direct.short": "Հասցեագրված",
+  "privacy.direct.long": "Թթել միայն նշուած օգտատէրերի համար",
+  "privacy.direct.short": "Հասցէագրուած",
   "privacy.private.long": "Թթել միայն հետեւողների համար",
   "privacy.private.short": "Միայն հետեւողներին",
   "privacy.public.long": "Թթել հրապարակային հոսքերում",
@@ -378,12 +378,12 @@
   "report.submit": "Ուղարկել",
   "report.target": "Բողոքել {target}֊ի մասին",
   "search.placeholder": "Փնտրել",
-  "search_popout.search_format": "Փնտրելու առաջադեմ ձեւ",
+  "search_popout.search_format": "Փնտրելու առաջադէմ ձեւ",
   "search_popout.tips.full_text": "Պարզ տեքստը վերադարձնում է գրառումներդ, հաւանածներդ, տարածածներդ, որտեղ ես նշուած եղել, ինչպէս նաեւ նման օգտանուններ, անուններ եւ պիտակներ։",
   "search_popout.tips.hashtag": "պիտակ",
   "search_popout.tips.status": "թութ",
-  "search_popout.tips.text": "Հասարակ տեքստը կվերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ",
-  "search_popout.tips.user": "օգտատեր",
+  "search_popout.tips.text": "Հասարակ տեքստը կը վերադարձնի համընկնող անուններ, օգտանուններ ու պիտակներ",
+  "search_popout.tips.user": "օգտատէր",
   "search_results.accounts": "Մարդիկ",
   "search_results.hashtags": "Պիտակներ",
   "search_results.statuses": "Թթեր",
@@ -394,14 +394,14 @@
   "status.block": "Արգելափակել @{name}֊ին",
   "status.bookmark": "Էջանիշ",
   "status.cancel_reblog_private": "Ապատարածել",
-  "status.cannot_reblog": "Այս թութը չի կարող տարածվել",
-  "status.copy": "Պատճենել գրառման հղումը",
+  "status.cannot_reblog": "Այս թութը չի կարող տարածուել",
+  "status.copy": "Պատճէնել գրառման յղումը",
   "status.delete": "Ջնջել",
-  "status.detailed_status": "Շղթայի ընդլայնված դիտում",
+  "status.detailed_status": "Շղթայի ընդլայնուած դիտում",
   "status.direct": "Նամակ գրել {name} -ին",
   "status.embed": "Ներդնել",
-  "status.favourite": "Հավանել",
-  "status.filtered": "Զտված",
+  "status.favourite": "Հաւանել",
+  "status.filtered": "Զտուած",
   "status.load_more": "Բեռնել աւելին",
   "status.media_hidden": "մեդիաբովանդակութիւնը թաքցուած է",
   "status.mention": "Նշել @{name}֊ին",
@@ -410,7 +410,7 @@
   "status.mute_conversation": "Լռեցնել խօսակցութիւնը",
   "status.open": "Ընդարձակել այս թութը",
   "status.pin": "Ամրացնել անձնական էջում",
-  "status.pinned": "Ամրացված թութ",
+  "status.pinned": "Ամրացուած թութ",
   "status.read_more": "Կարդալ աւելին",
   "status.reblog": "Տարածել",
   "status.reblog_private": "Տարածել սեփական լսարանին",
@@ -419,10 +419,10 @@
   "status.redraft": "Ջնջել եւ վերակազմել",
   "status.remove_bookmark": "Հեռացնել էջանիշերից",
   "status.reply": "Պատասխանել",
-  "status.replyAll": "Պատասխանել թելին",
+  "status.replyAll": "Պատասխանել շղթային",
   "status.report": "Բողոքել @{name}֊ից",
   "status.sensitive_warning": "Կասկածելի բովանդակութիւն",
-  "status.share": "Կիսվել",
+  "status.share": "Կիսուել",
   "status.show_less": "Պակաս",
   "status.show_less_all": "Թաքցնել բոլոր նախազգուշացնումները",
   "status.show_more": "Աւելին",
@@ -432,7 +432,7 @@
   "status.unmute_conversation": "Ապալռեցնել խօսակցութիւնը",
   "status.unpin": "Հանել անձնական էջից",
   "suggestions.dismiss": "Անտեսել առաջարկը",
-  "suggestions.header": "Միգուցե քեզ հետաքրքրի…",
+  "suggestions.header": "Միգուցէ քեզ հետաքրքրի…",
   "tabs_bar.federated_timeline": "Դաշնային",
   "tabs_bar.home": "Հիմնական",
   "tabs_bar.local_timeline": "Տեղական",
@@ -440,41 +440,41 @@
   "tabs_bar.search": "Փնտրել",
   "time_remaining.days": "{number, plural, one {մնաց # օր} other {մնաց # օր}}",
   "time_remaining.hours": "{number, plural, one {# ժամ} other {# ժամ}} անց",
-  "time_remaining.minutes": "{number, plural, one {# րոպե} other {# րոպե}} անց",
+  "time_remaining.minutes": "{number, plural, one {# րոպէ} other {# րոպէ}} անց",
   "time_remaining.moments": "Մնացել է մի քանի վարկեան",
   "time_remaining.seconds": "{number, plural, one {# վարկեան} other {# վարկեան}} անց",
   "timeline_hint.remote_resource_not_displayed": "{resource} այլ սպասարկիչներից չեն ցուցադրվել:",
   "timeline_hint.resources.followers": "Հետևորդներ",
-  "timeline_hint.resources.follows": "Հետևել",
+  "timeline_hint.resources.follows": "Հետեւել",
   "timeline_hint.resources.statuses": "Հին թութեր",
   "trends.counter_by_accounts": "{count, plural, one {{counter} մարդ} other {{counter} մարդիկ}} խօսում են",
   "trends.trending_now": "Այժմ արդիական",
-  "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
+  "ui.beforeunload": "Քո սեւագիրը կը կորի, եթէ լքես Մաստոդոնը։",
   "units.short.billion": "{count}մլրդ",
   "units.short.million": "{count}մլն",
   "units.short.thousand": "{count}Հազ.",
   "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
-  "upload_button.label": "Ավելացնել մեդիա",
-  "upload_error.limit": "Ֆայլի վերբեռնման սահմանաչափը գերազանցված է։",
+  "upload_button.label": "Աւելացնել մեդիա",
+  "upload_error.limit": "Նիշքի վերբեռնման սահմանաչափը գերազանցուած է։",
   "upload_error.poll": "Հարցումների հետ նիշք կցել հնարաւոր չէ։",
   "upload_form.audio_description": "Նկարագրիր ձայնագրութեան բովանդակութիւնը լսողական խնդիրներով անձանց համար",
   "upload_form.description": "Նկարագիր՝ տեսողական խնդիրներ ունեցողների համար",
   "upload_form.edit": "Խմբագրել",
   "upload_form.thumbnail": "Փոխել պատկերակը",
-  "upload_form.undo": "Հետարկել",
+  "upload_form.undo": "Յետարկել",
   "upload_form.video_description": "Նկարագրիր տեսանիւթը լսողական կամ տեսողական խնդիրներով անձանց համար",
   "upload_modal.analyzing_picture": "Լուսանկարի վերլուծում…",
   "upload_modal.apply": "Կիրառել",
   "upload_modal.choose_image": "Ընտրել նկար",
-  "upload_modal.description_placeholder": "Բել դղյակի ձախ ժամն օֆ ազգությանը ցպահանջ չճշտած վնաս էր եւ փառք։",
-  "upload_modal.detect_text": "Հայտնբերել տեքստը նկարից",
+  "upload_modal.description_placeholder": "Բել դղեակի ձախ ժամն օֆ ազգութեանը ցպահանջ չճշտած վնաս էր եւ փառք։",
+  "upload_modal.detect_text": "Յայտնաբերել տեքստը նկարից",
   "upload_modal.edit_media": "Խմբագրել մեդիան",
   "upload_modal.hint": "Սեղմէք եւ տեղաշարժէք նախադիտման շրջանակը՝ որ ընտրէք մանրապատկերում միշտ տեսանելի կէտը։",
   "upload_modal.preparing_ocr": "Գրաճանաչման նախապատրաստում…",
   "upload_modal.preview_label": "Նախադիտում ({ratio})",
   "upload_progress.label": "Վերբեռնվում է…",
   "video.close": "Փակել  տեսագրութիւնը",
-  "video.download": "Ներբեռնել ֆայլը",
+  "video.download": "Ներբեռնել նիշքը",
   "video.exit_fullscreen": "Անջատել լիաէկրան դիտումը",
   "video.expand": "Ընդարձակել տեսագրութիւնը",
   "video.fullscreen": "Լիաէկրան",
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 97b4ec401..4f44f3800 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -38,7 +38,7 @@
   "account.requested": "അനുവാദത്തിനായി കാത്തിരിക്കുന്നു. പിന്തുടരാനുള്ള അപേക്ഷ റദ്ദാക്കുവാൻ ഞെക്കുക",
   "account.share": "@{name} ന്റെ പ്രൊഫൈൽ പങ്കുവെക്കുക",
   "account.show_reblogs": "@{name} ൽ നിന്നുള്ള ബൂസ്റ്റുകൾ കാണിക്കുക",
-  "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
+  "account.statuses_counter": "{count, plural, one {{counter} ടൂട്ട്} other {{counter} ടൂട്ടുകൾ}}",
   "account.unblock": "ബ്ലോക്ക് മാറ്റുക @{name}",
   "account.unblock_domain": "{domain} വെളിപ്പെടുത്തുക",
   "account.unendorse": "പ്രൊഫൈലിൽ പ്രകടമാക്കാതിരിക്കുക",
@@ -250,8 +250,8 @@
   "keyboard_shortcuts.spoilers": "to show/hide CW field",
   "keyboard_shortcuts.start": "to open \"get started\" column",
   "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
-  "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.toggle_sensitivity": "മീഡിയ കാണിക്കുന്നതിനും/മറയ്ക്കുന്നതിനും",
+  "keyboard_shortcuts.toot": "ഒരു പുതിയ ടൂട്ട് ആരംഭിക്കാൻ",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
   "keyboard_shortcuts.up": "to move up in the list",
   "lightbox.close": "അടയ്ക്കുക",
@@ -269,7 +269,7 @@
   "lists.replies_policy.followed": "Any followed user",
   "lists.replies_policy.list": "Members of the list",
   "lists.replies_policy.none": "ആരുമില്ല",
-  "lists.replies_policy.title": "Show replies to:",
+  "lists.replies_policy.title": "ഇതിനുള്ള മറുപടികൾ കാണിക്കുക:",
   "lists.search": "Search among people you follow",
   "lists.subheading": "എന്റെ പട്ടികകൾ",
   "load_pending": "{count, plural, one {# new item} other {# new items}}",
@@ -305,14 +305,14 @@
   "navigation_bar.security": "സുരക്ഷ",
   "notification.favourite": "{name} favourited your status",
   "notification.follow": "{name} നിങ്ങളെ പിന്തുടർന്നു",
-  "notification.follow_request": "{name} has requested to follow you",
+  "notification.follow_request": "{name} നിങ്ങളെ പിന്തുടരാൻ അഭ്യർത്ഥിച്ചു",
   "notification.mention": "{name} mentioned you",
   "notification.own_poll": "നിങ്ങളുടെ പോൾ അവസാനിച്ചു",
   "notification.poll": "A poll you have voted in has ended",
   "notification.reblog": "{name} നിങ്ങളുടെ പോസ്റ്റ് ബൂസ്റ്റ് ചെയ്തു",
   "notification.status": "{name} ഇപ്പോൾ പോസ്റ്റുചെയ്‌തു",
   "notifications.clear": "അറിയിപ്പ് മായ്ക്കുക",
-  "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
+  "notifications.clear_confirmation": "നിങ്ങളുടെ എല്ലാ അറിയിപ്പുകളും ശാശ്വതമായി മായ്‌ക്കണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?",
   "notifications.column_settings.alert": "ഡെസ്ക്ടോപ്പ് അറിയിപ്പുകൾ",
   "notifications.column_settings.favourite": "പ്രിയപ്പെട്ടവ:",
   "notifications.column_settings.filter_bar.advanced": "എല്ലാ വിഭാഗങ്ങളും പ്രദർശിപ്പിക്കുക",
@@ -349,14 +349,14 @@
   "poll.total_people": "{count, plural, one {# person} other {# people}}",
   "poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
   "poll.vote": "വോട്ട് ചെയ്യുക",
-  "poll.voted": "You voted for this answer",
+  "poll.voted": "ഈ ഉത്തരത്തിനായി നിങ്ങൾ വോട്ട് ചെയ്തു",
   "poll_button.add_poll": "ഒരു പോൾ ചേർക്കുക",
   "poll_button.remove_poll": "പോൾ നീക്കംചെയ്യുക",
-  "privacy.change": "Adjust status privacy",
+  "privacy.change": "ടൂട്ട് സ്വകാര്യത ക്രമീകരിക്കുക",
   "privacy.direct.long": "Post to mentioned users only",
   "privacy.direct.short": "നേരിട്ട്",
   "privacy.private.long": "Post to followers only",
-  "privacy.private.short": "Followers-only",
+  "privacy.private.short": "പിന്തുടരുന്നവർക്ക് മാത്രം",
   "privacy.public.long": "Post to public timelines",
   "privacy.public.short": "Public",
   "privacy.unlisted.long": "Do not show in public timelines",
@@ -372,7 +372,7 @@
   "relative_time.today": "ഇന്ന്",
   "reply_indicator.cancel": "റദ്ദാക്കുക",
   "report.forward": "Forward to {target}",
-  "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
+  "report.forward_hint": "ഈ അക്കൗണ്ട് മറ്റൊരു സെർവറിൽ നിന്നാണ്. റിപ്പോർട്ടിന്റെ അജ്ഞാത പകർപ്പ് അവിടെ അയയ്ക്കണോ?",
   "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
   "report.placeholder": "കൂടുതൽ അഭിപ്രായങ്ങൾ",
   "report.submit": "സമർപ്പിക്കുക",
@@ -420,14 +420,14 @@
   "status.remove_bookmark": "ബുക്ക്മാർക്ക് നീക്കംചെയ്യുക",
   "status.reply": "മറുപടി",
   "status.replyAll": "Reply to thread",
-  "status.report": "Report @{name}",
+  "status.report": "@{name}--നെ റിപ്പോർട്ട് ചെയ്യുക",
   "status.sensitive_warning": "Sensitive content",
   "status.share": "പങ്കിടുക",
   "status.show_less": "കുറച്ച് കാണിക്കുക",
   "status.show_less_all": "Show less for all",
   "status.show_more": "കൂടുതകൽ കാണിക്കുക",
   "status.show_more_all": "എല്ലാവർക്കുമായി കൂടുതൽ കാണിക്കുക",
-  "status.show_thread": "Show thread",
+  "status.show_thread": "ത്രെഡ് കാണിക്കുക",
   "status.uncached_media_warning": "ലഭ്യമല്ല",
   "status.unmute_conversation": "Unmute conversation",
   "status.unpin": "Unpin from profile",
@@ -463,24 +463,24 @@
   "upload_form.thumbnail": "ലഘുചിത്രം മാറ്റുക",
   "upload_form.undo": "ഇല്ലാതാക്കുക",
   "upload_form.video_description": "Describe for people with hearing loss or visual impairment",
-  "upload_modal.analyzing_picture": "Analyzing picture…",
+  "upload_modal.analyzing_picture": "ചിത്രം വിശകലനം ചെയ്യുന്നു…",
   "upload_modal.apply": "പ്രയോഗിക്കുക",
   "upload_modal.choose_image": "ചിത്രം തിരഞ്ഞെടുക്കുക",
   "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
   "upload_modal.detect_text": "Detect text from picture",
   "upload_modal.edit_media": "മീഡിയ തിരുത്തുക",
   "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
-  "upload_modal.preparing_ocr": "Preparing OCR…",
+  "upload_modal.preparing_ocr": "OCR തയ്യാറാക്കുന്നു…",
   "upload_modal.preview_label": "Preview ({ratio})",
   "upload_progress.label": "Uploading…",
   "video.close": "വീഡിയോ അടയ്ക്കുക",
   "video.download": "ഫയൽ ഡൌൺലോഡ് ചെയ്യുക",
-  "video.exit_fullscreen": "Exit full screen",
+  "video.exit_fullscreen": "പൂർണ്ണ സ്ക്രീനിൽ നിന്ന് പുറത്തുകടക്കുക",
   "video.expand": "Expand video",
   "video.fullscreen": "പൂർണ്ണ സ്ക്രീൻ",
   "video.hide": "വീഡിയോ മറയ്ക്കുക",
   "video.mute": "Mute sound",
   "video.pause": "Pause",
-  "video.play": "Play",
+  "video.play": "പ്ലേ",
   "video.unmute": "Unmute sound"
 }
diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json
index 14c74830b..d22b8d704 100644
--- a/app/javascript/mastodon/locales/vi.json
+++ b/app/javascript/mastodon/locales/vi.json
@@ -1,5 +1,5 @@
 {
-  "account.account_note_header": "Viết nhận xét",
+  "account.account_note_header": "Ghi chú",
   "account.add_or_remove_from_list": "Thêm hoặc Xóa khỏi danh sách",
   "account.badges.bot": "Bot",
   "account.badges.group": "Nhóm",
@@ -425,10 +425,10 @@
   "status.share": "Chia sẻ",
   "status.show_less": "Thu gọn",
   "status.show_less_all": "Thu gọn toàn bộ",
-  "status.show_more": "Mở rộng",
+  "status.show_more": "Xem thêm",
   "status.show_more_all": "Hiển thị tất cả",
-  "status.show_thread": "Liên quan",
-  "status.uncached_media_warning": "Giới hạn",
+  "status.show_thread": "Toàn bộ chủ đề",
+  "status.uncached_media_warning": "Uncached",
   "status.unmute_conversation": "Quan tâm",
   "status.unpin": "Bỏ ghim trên trang cá nhân",
   "suggestions.dismiss": "Tắt đề xuất",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 6ff640f8d..c9a2e75b0 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -154,6 +154,11 @@
     display: block;
     width: 100%;
   }
+
+  .layout-multiple-columns &.button--with-bell {
+    font-size: 12px;
+    padding: 0 8px;
+  }
 }
 
 .column__wrapper {
@@ -2879,6 +2884,7 @@ a.account__display-name {
     flex: 0 0 auto;
     padding: 10px;
     padding-top: 20px;
+    z-index: 1;
 
     ul {
       margin-bottom: 10px;
@@ -7194,21 +7200,6 @@ noscript {
   .audio-player {
     border-radius: 0;
   }
-
-  @media screen and (max-width: 415px) {
-    width: 210px;
-    bottom: 10px;
-    right: 10px;
-
-    &__footer {
-      display: none;
-    }
-
-    .video-player,
-    .audio-player {
-      border-radius: 0 0 4px 4px;
-    }
-  }
 }
 
 .picture-in-picture-placeholder {
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 51d9b46b0..e40ad18ff 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -444,6 +444,10 @@
       }
     }
 
+    .logo-button {
+      padding: 3px 15px;
+    }
+
     &__image {
       border-radius: 4px 4px 0 0;
       overflow: hidden;
diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss
index 8051e4edb..baacf46b9 100644
--- a/app/javascript/styles/mastodon/rtl.scss
+++ b/app/javascript/styles/mastodon/rtl.scss
@@ -170,6 +170,11 @@ body.rtl {
     right: 42px;
   }
 
+  .account__header__tabs__buttons > .icon-button {
+    margin-right: 0;
+    margin-left: 8px;
+  }
+
   .account__avatar-overlay-overlay {
     right: auto;
     left: 0;
diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss
index 7ae1c5a24..078714325 100644
--- a/app/javascript/styles/mastodon/statuses.scss
+++ b/app/javascript/styles/mastodon/statuses.scss
@@ -83,9 +83,14 @@
   background: $ui-highlight-color;
   color: $primary-text-color;
   text-transform: none;
-  line-height: 36px;
+  line-height: 1.2;
   height: auto;
-  padding: 3px 15px;
+  min-height: 36px;
+  min-width: 88px;
+  white-space: normal;
+  overflow-wrap: break-word;
+  hyphens: auto;
+  padding: 0 15px;
   border: 0;
 
   svg {
@@ -126,6 +131,12 @@
   }
 }
 
+a.button.logo-button {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
 .embed,
 .public-layout {
   .status__content[data-spoiler=folded] {
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index e2c4b8acf..974f57820 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -97,8 +97,8 @@ module AccountInteractions
     has_many :announcement_mutes, dependent: :destroy
   end
 
-  def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
-    rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
+  def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
+    rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
                               .find_or_create_by!(target_account: other_account)
 
     rel.show_reblogs = reblogs unless reblogs.nil?
@@ -111,8 +111,8 @@ module AccountInteractions
     rel
   end
 
-  def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
-    rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
+  def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false, bypass_limit: false)
+    rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
                          .find_or_create_by!(target_account: other_account)
 
     rel.show_reblogs = reblogs unless reblogs.nil?
diff --git a/app/models/concerns/follow_limitable.rb b/app/models/concerns/follow_limitable.rb
new file mode 100644
index 000000000..c64060d6e
--- /dev/null
+++ b/app/models/concerns/follow_limitable.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module FollowLimitable
+  extend ActiveSupport::Concern
+
+  included do
+    validates_with FollowLimitValidator, on: :create, unless: :bypass_follow_limit?
+  end
+
+  def bypass_follow_limit=(value)
+    @bypass_follow_limit = value
+  end
+
+  def bypass_follow_limit?
+    @bypass_follow_limit
+  end
+end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index 69a1722b3..a5e3fe809 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -17,6 +17,7 @@ class Follow < ApplicationRecord
   include Paginable
   include RelationshipCacheable
   include RateLimitable
+  include FollowLimitable
 
   rate_limit by: :account, family: :follows
 
@@ -26,7 +27,6 @@ class Follow < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
 
   validates :account_id, uniqueness: { scope: :target_account_id }
-  validates_with FollowLimitValidator, on: :create, if: :rate_limit?
 
   scope :recent, -> { reorder(id: :desc) }
 
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 2d2a77b59..59fefcdf6 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -17,6 +17,7 @@ class FollowRequest < ApplicationRecord
   include Paginable
   include RelationshipCacheable
   include RateLimitable
+  include FollowLimitable
 
   rate_limit by: :account, family: :follows
 
@@ -26,7 +27,6 @@ class FollowRequest < ApplicationRecord
   has_one :notification, as: :activity, dependent: :destroy
 
   validates :account_id, uniqueness: { scope: :target_account_id }
-  validates_with FollowLimitValidator, on: :create, if: :rate_limit?
 
   def authorize!
     account.follow!(target_account, reblogs: show_reblogs, notify: notify, uri: uri)
diff --git a/app/serializers/rest/account_featured_tag_serializer.rb b/app/serializers/rest/account_featured_tag_serializer.rb
deleted file mode 100644
index 84bef2e62..000000000
--- a/app/serializers/rest/account_featured_tag_serializer.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class REST::AccountFeaturedTagSerializer < ActiveModel::Serializer
-  include RoutingHelper
-
-  attributes :id, :name, :url
-
-  def id
-    object.tag.id.to_s
-  end
-
-  def url
-    short_account_tag_url(object.account, object.tag)
-  end
-end
diff --git a/app/serializers/rest/featured_tag_serializer.rb b/app/serializers/rest/featured_tag_serializer.rb
index 08121ff16..96adcc7d0 100644
--- a/app/serializers/rest/featured_tag_serializer.rb
+++ b/app/serializers/rest/featured_tag_serializer.rb
@@ -1,9 +1,15 @@
 # frozen_string_literal: true
 
 class REST::FeaturedTagSerializer < ActiveModel::Serializer
-  attributes :id, :name, :statuses_count, :last_status_at
+  include RoutingHelper
+
+  attributes :id, :name, :url, :statuses_count, :last_status_at
 
   def id
     object.id.to_s
   end
+
+  def url
+    short_account_tag_url(object.account, object.tag)
+  end
 end
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 2b649ee22..363aa5ccf 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -32,7 +32,7 @@ class BatchedRemoveStatusService < BaseService
 
     # Since we skipped all callbacks, we also need to manually
     # deindex the statuses
-    Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs) if Chewy.enabled?
+    Chewy.strategy.current.update(StatusesIndex::Status, statuses_and_reblogs) if Chewy.enabled?
 
     return if options[:skip_side_effects]
 
diff --git a/app/services/delete_account_service.rb b/app/services/delete_account_service.rb
index 2bb533cfb..802799ccd 100644
--- a/app/services/delete_account_service.rb
+++ b/app/services/delete_account_service.rb
@@ -142,6 +142,7 @@ class DeleteAccountService < BaseService
     purge_user!
     purge_profile!
     purge_statuses!
+    purge_mentions!
     purge_media_attachments!
     purge_polls!
     purge_generated_notifications!
@@ -159,6 +160,10 @@ class DeleteAccountService < BaseService
     end
   end
 
+  def purge_mentions!
+    @account.mentions.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
+  end
+
   def purge_media_attachments!
     @account.media_attachments.reorder(nil).find_each do |media_attachment|
       next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)
@@ -182,7 +187,7 @@ class DeleteAccountService < BaseService
     @account.favourites.in_batches do |favourites|
       ids = favourites.pluck(:status_id)
       StatusStat.where(status_id: ids).update_all('favourites_count = GREATEST(0, favourites_count - 1)')
-      Chewy.strategy.current.update(StatusesIndex, ids) if Chewy.enabled?
+      Chewy.strategy.current.update(StatusesIndex::Status, ids) if Chewy.enabled?
       # Rails.cache.delete_multi would be better, but we don't have it yet
       ids.each { |id| Rails.cache.delete("statuses/#{id}") }
       favourites.delete_all
@@ -191,7 +196,7 @@ class DeleteAccountService < BaseService
 
   def purge_bookmarks!
     @account.bookmarks.in_batches do |bookmarks|
-      Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled?
+      Chewy.strategy.current.update(StatusesIndex::Status, bookmarks.pluck(:status_id)) if Chewy.enabled?
       bookmarks.delete_all
     end
   end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 962572851..b98f7011d 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -11,11 +11,12 @@ class FollowService < BaseService
   # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
   # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
   # @option [Boolean] :bypass_locked
+  # @option [Boolean] :bypass_limit Allow following past the total follow number
   # @option [Boolean] :with_rate_limit
   def call(source_account, target_account, options = {})
     @source_account = source_account
     @target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
-    @options        = { bypass_locked: false, with_rate_limit: false }.merge(options)
+    @options        = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)
 
     raise ActiveRecord::RecordNotFound if following_not_possible?
     raise Mastodon::NotPermittedError  if following_not_allowed?
@@ -54,7 +55,7 @@ class FollowService < BaseService
   end
 
   def request_follow!
-    follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
+    follow_request = @source_account.request_follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
 
     if @target_account.local?
       LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, :follow_request)
@@ -66,7 +67,7 @@ class FollowService < BaseService
   end
 
   def direct_follow!
-    follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit])
+    follow = @source_account.follow!(@target_account, reblogs: @options[:reblogs], notify: @options[:notify], rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit])
 
     LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, :follow)
     MergeWorker.perform_async(@target_account.id, @source_account.id)
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 19d65280d..9f4da91d4 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -65,10 +65,16 @@ class SuspendAccountService < BaseService
         attachment = media_attachment.public_send(attachment_name)
         styles     = [:original] | attachment.styles.keys
 
+        next if attachment.blank?
+
         styles.each do |style|
           case Paperclip::Attachment.default_options[:storage]
           when :s3
-            attachment.s3_object(style).acl.put(acl: 'private')
+            begin
+              attachment.s3_object(style).acl.put(acl: 'private')
+            rescue Aws::S3::Errors::NoSuchKey
+              Rails.logger.warn "Tried to change acl on non-existent key #{attachment.s3_object(style).key}"
+            end
           when :fog
             # Not supported
           when :filesystem
diff --git a/app/services/unsuspend_account_service.rb b/app/services/unsuspend_account_service.rb
index f07a3f053..ce9ee48ed 100644
--- a/app/services/unsuspend_account_service.rb
+++ b/app/services/unsuspend_account_service.rb
@@ -56,10 +56,16 @@ class UnsuspendAccountService < BaseService
         attachment = media_attachment.public_send(attachment_name)
         styles     = [:original] | attachment.styles.keys
 
+        next if attachment.blank?
+
         styles.each do |style|
           case Paperclip::Attachment.default_options[:storage]
           when :s3
-            attachment.s3_object(style).acl.put(acl: Paperclip::Attachment.default_options[:s3_permissions])
+            begin
+              attachment.s3_object(style).acl.put(acl: Paperclip::Attachment.default_options[:s3_permissions])
+            rescue Aws::S3::Errors::NoSuchKey
+              Rails.logger.warn "Tried to change acl on non-existent key #{attachment.s3_object(style).key}"
+            end
           when :fog
             # Not supported
           when :filesystem
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index fa15796d2..ada6dd2bc 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -14,7 +14,7 @@
     - unless status.proper.media_attachments.empty?
       - if status.proper.media_attachments.first.video?
         - video = status.proper.media_attachments.first
-        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.proper.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description
+        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: status.proper.sensitive?, visible: false, width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json
       - elsif status.proper.media_attachments.first.audio?
         - audio = status.proper.media_attachments.first
         = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration)
diff --git a/app/views/media/player.html.haml b/app/views/media/player.html.haml
index 7369628a4..191586248 100644
--- a/app/views/media/player.html.haml
+++ b/app/views/media/player.html.haml
@@ -10,7 +10,7 @@
   = render partial: 'layouts/theme', object: @theme
 
 - if @media_attachment.video?
-  = react_component :video, src: @media_attachment.file.url(:original), preview: @media_attachment.thumbnail.present? ? @media_attachment.thumbnail.url : @media_attachment.file.url(:small), blurhash: @media_attachment.blurhash, width: 670, height: 380, editable: true, detailed: true, inline: true, alt: @media_attachment.description do
+  = react_component :video, src: @media_attachment.file.url(:original), preview: @media_attachment.thumbnail.present? ? @media_attachment.thumbnail.url : @media_attachment.file.url(:small), frameRate: @media_attachment.file.meta.dig('original', 'frame_rate'), blurhash: @media_attachment.blurhash, width: 670, height: 380, editable: true, detailed: true, inline: true, alt: @media_attachment.description, media: [ActiveModelSerializers::SerializableResource.new(@media_attachment, serializer: REST::MediaAttachmentSerializer)].as_json do
     %video{ controls: 'controls' }
       %source{ src: @media_attachment.file.url(:original) }
 - elsif @media_attachment.gifv?
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 4c879472d..93af131e5 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -29,7 +29,7 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 670, height: 380, detailed: true, inline: true, alt: video.description do
+      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 670, height: 380, detailed: true, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - elsif status.media_attachments.first.audio?
       - audio = status.media_attachments.first
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index 1b501a3ef..ab163245d 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -38,7 +38,7 @@
   - if !status.media_attachments.empty?
     - if status.media_attachments.first.video?
       - video = status.media_attachments.first
-      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 610, height: 343, inline: true, alt: video.description do
+      = react_component :video, src: full_asset_url(video.file.url(:original)), preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: sensitized?(status, current_account), width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json do
         = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
     - elsif status.media_attachments.first.audio?
       - audio = status.media_attachments.first
diff --git a/app/workers/authorize_follow_worker.rb b/app/workers/authorize_follow_worker.rb
index 0d5014624..f57900fa5 100644
--- a/app/workers/authorize_follow_worker.rb
+++ b/app/workers/authorize_follow_worker.rb
@@ -7,7 +7,7 @@ class AuthorizeFollowWorker
     source_account = Account.find(source_account_id)
     target_account = Account.find(target_account_id)
 
-    AuthorizeFollowService.new.call(source_account, target_account)
+    AuthorizeFollowService.new.call(source_account, target_account, bypass_limit: true)
   rescue ActiveRecord::RecordNotFound
     true
   end
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
index 4a455f3ae..4a7100435 100644
--- a/app/workers/import/relationship_worker.rb
+++ b/app/workers/import/relationship_worker.rb
@@ -15,7 +15,11 @@ class Import::RelationshipWorker
 
     case relationship
     when 'follow'
-      FollowService.new.call(from_account, target_account, options)
+      begin
+        FollowService.new.call(from_account, target_account, options)
+      rescue ActiveRecord::RecordInvalid
+        raise if FollowLimitValidator.limit_for_account(from_account) < from_account.following_count
+      end
     when 'unfollow'
       UnfollowService.new.call(from_account, target_account)
     when 'block'
diff --git a/app/workers/refollow_worker.rb b/app/workers/refollow_worker.rb
index 98940680d..319b00109 100644
--- a/app/workers/refollow_worker.rb
+++ b/app/workers/refollow_worker.rb
@@ -19,7 +19,7 @@ class RefollowWorker
 
       # Schedule re-follow
       begin
-        FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify)
+        FollowService.new.call(follower, target_account, reblogs: reblogs, notify: notify, bypass_limit: true)
       rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
         next
       end
diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb
index 71b5a0e3f..0bd5ff472 100644
--- a/app/workers/unfollow_follow_worker.rb
+++ b/app/workers/unfollow_follow_worker.rb
@@ -14,7 +14,7 @@ class UnfollowFollowWorker
     reblogs = follow&.show_reblogs?
     notify  = follow&.notify?
 
-    FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked)
+    FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, bypass_locked: bypass_locked, bypass_limit: true)
     UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
   rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
     true