about summary refs log tree commit diff
diff options
context:
space:
mode:
authorReverite <github@reverite.sh>2019-05-21 20:48:57 -0700
committerReverite <github@reverite.sh>2019-05-21 20:48:57 -0700
commitd873e4f3d5878a2b2800758cbe3b9c870c228d51 (patch)
tree23a3a373699381d4bd4f629136b9c82de5df96a3
parent4cea89bf2e9ce7f73fccfc637191b1e039ad25ee (diff)
parent2332b3f146b0d879daba8a99bd35c8bf425edea3 (diff)
Merge branch 'glitch' into production
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock28
-rw-r--r--app/controllers/api/v1/statuses_controller.rb2
-rw-r--r--app/controllers/settings/preferences_controller.rb1
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js9
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js5
-rw-r--r--app/javascript/flavours/glitch/containers/poll_container.js2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/options.js56
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/options_container.js10
-rw-r--r--app/javascript/flavours/glitch/features/compose/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js8
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js11
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js1
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js1
-rw-r--r--app/javascript/mastodon/components/poll.js14
-rw-r--r--app/javascript/mastodon/components/status.js2
-rw-r--r--app/javascript/mastodon/features/compose/containers/compose_form_container.js2
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/lib/formatter.rb107
-rw-r--r--app/lib/user_settings_decorator.rb5
-rw-r--r--app/models/status.rb2
-rw-r--r--app/models/user.rb8
-rw-r--r--app/serializers/initial_state_serializer.rb1
-rw-r--r--app/serializers/rest/status_serializer.rb1
-rw-r--r--app/services/post_status_service.rb1
-rw-r--r--app/services/reblog_service.rb4
-rw-r--r--app/validators/blacklisted_email_validator.rb2
-rw-r--r--app/views/settings/preferences/show.html.haml2
-rw-r--r--app/views/stream_entries/_og_image.html.haml2
-rw-r--r--config/locales/co.yml4
-rw-r--r--config/locales/cs.yml4
-rw-r--r--config/locales/simple_form.en.yml7
-rw-r--r--config/locales/sk.yml38
-rw-r--r--config/settings.yml1
-rw-r--r--db/migrate/20190512200918_add_content_type_to_statuses.rb5
-rw-r--r--db/post_migrate/20190519130537_remove_boosts_widening_audience.rb23
-rw-r--r--db/schema.rb3
-rw-r--r--docker-compose.yml2
-rw-r--r--spec/services/reblog_service_spec.rb12
-rw-r--r--spec/validators/blacklisted_email_validator_spec.rb2
41 files changed, 330 insertions, 72 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7eceaf142..e4ec9e3c2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -52,9 +52,7 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https://
 
 ## Translations
 
-You can submit translations via [Weblate](https://weblate.joinmastodon.org/). They are periodically merged into the codebase.
-
-[![Mastodon translation statistics by language](https://weblate.joinmastodon.org/widgets/mastodon/-/multi-auto.svg)](https://weblate.joinmastodon.org/)
+You can submit translations via pull request.
 
 ## Pull requests
 
diff --git a/Gemfile b/Gemfile
index b9a9159de..6c7bef290 100644
--- a/Gemfile
+++ b/Gemfile
@@ -15,7 +15,7 @@ gem 'makara', '~> 0.4'
 gem 'pghero', '~> 2.2'
 gem 'dotenv-rails', '~> 2.7'
 
-gem 'aws-sdk-s3', '~> 1.36', require: false
+gem 'aws-sdk-s3', '~> 1.39', require: false
 gem 'fog-core', '<= 2.1.0'
 gem 'fog-openstack', '~> 0.3', require: false
 gem 'paperclip', '~> 6.0'
@@ -95,6 +95,8 @@ gem 'json-ld', '~> 3.0'
 gem 'json-ld-preloaded', '~> 3.0'
 gem 'rdf-normalize', '~> 0.3'
 
+gem 'redcarpet', '~> 3.4'
+
 group :development, :test do
   gem 'fabrication', '~> 2.20'
   gem 'fuubar', '~> 2.3'
@@ -109,7 +111,7 @@ group :production, :test do
 end
 
 group :test do
-  gem 'capybara', '~> 3.19'
+  gem 'capybara', '~> 3.20'
   gem 'climate_control', '~> 0.2'
   gem 'faker', '~> 1.9'
   gem 'microformats', '~> 4.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 98cd2103d..4dacad5e4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -75,18 +75,18 @@ GEM
       encryptor (~> 3.0.0)
     av (0.9.0)
       cocaine (~> 0.5.3)
-    aws-eventstream (1.0.2)
-    aws-partitions (1.151.0)
-    aws-sdk-core (3.48.4)
+    aws-eventstream (1.0.3)
+    aws-partitions (1.162.0)
+    aws-sdk-core (3.52.1)
       aws-eventstream (~> 1.0, >= 1.0.2)
       aws-partitions (~> 1.0)
       aws-sigv4 (~> 1.1)
       jmespath (~> 1.0)
-    aws-sdk-kms (1.17.0)
-      aws-sdk-core (~> 3, >= 3.48.2)
+    aws-sdk-kms (1.20.0)
+      aws-sdk-core (~> 3, >= 3.52.1)
       aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.36.1)
-      aws-sdk-core (~> 3, >= 3.48.2)
+    aws-sdk-s3 (1.39.0)
+      aws-sdk-core (~> 3, >= 3.52.1)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.0)
     aws-sigv4 (1.1.0)
@@ -129,13 +129,14 @@ GEM
       sshkit (~> 1.3)
     capistrano-yarn (2.0.2)
       capistrano (~> 3.0)
-    capybara (3.19.1)
+    capybara (3.20.0)
       addressable
       mini_mime (>= 0.1.3)
       nokogiri (~> 1.8)
       rack (>= 1.6.0)
       rack-test (>= 0.6.3)
       regexp_parser (~> 1.2)
+      uglifier
       xpath (~> 3.2)
     case_transform (0.2)
       activesupport
@@ -207,6 +208,7 @@ GEM
     et-orbi (1.1.6)
       tzinfo
     excon (0.62.0)
+    execjs (2.7.0)
     fabrication (2.20.2)
     faker (1.9.3)
       i18n (>= 0.7)
@@ -480,6 +482,7 @@ GEM
       link_header (~> 0.0, >= 0.0.8)
     rdf-normalize (0.3.3)
       rdf (>= 2.2, < 4.0)
+    redcarpet (3.4.0)
     redis (4.1.1)
     redis-actionpack (5.0.2)
       actionpack (>= 4.0, < 6)
@@ -499,7 +502,7 @@ GEM
       redis-store (>= 1.2, < 2)
     redis-store (1.5.0)
       redis (>= 2.2, < 5)
-    regexp_parser (1.4.0)
+    regexp_parser (1.5.0)
     request_store (1.4.1)
       rack (>= 1.4)
     responders (2.4.1)
@@ -627,6 +630,8 @@ GEM
       thread_safe (~> 0.1)
     tzinfo-data (1.2019.1)
       tzinfo (>= 1.0.0)
+    uglifier (4.1.20)
+      execjs (>= 0.3.0, < 3)
     unf (0.1.4)
       unf_ext
     unf_ext (0.0.7.5)
@@ -660,7 +665,7 @@ DEPENDENCIES
   active_record_query_trace (~> 1.6)
   addressable (~> 2.6)
   annotate (~> 2.7)
-  aws-sdk-s3 (~> 1.36)
+  aws-sdk-s3 (~> 1.39)
   better_errors (~> 2.5)
   binding_of_caller (~> 0.7)
   blurhash (~> 0.1)
@@ -673,7 +678,7 @@ DEPENDENCIES
   capistrano-rails (~> 1.4)
   capistrano-rbenv (~> 2.1)
   capistrano-yarn (~> 2.0)
-  capybara (~> 3.19)
+  capybara (~> 3.20)
   charlock_holmes (~> 0.7.6)
   chewy (~> 5.0)
   cld3 (~> 3.2.4)
@@ -745,6 +750,7 @@ DEPENDENCIES
   rails-i18n (~> 5.1)
   rails-settings-cached (~> 0.6)
   rdf-normalize (~> 0.3)
+  redcarpet (~> 3.4)
   redis (~> 4.1)
   redis-namespace (~> 1.5)
   redis-rails (~> 5.0)
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index b0e134554..26a0ab457 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -54,6 +54,7 @@ class Api::V1::StatusesController < Api::BaseController
                                          scheduled_at: status_params[:scheduled_at],
                                          application: doorkeeper_token.application,
                                          poll: status_params[:poll],
+                                         content_type: status_params[:content_type],
                                          idempotency: request.headers['Idempotency-Key'])
 
     render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
@@ -85,6 +86,7 @@ class Api::V1::StatusesController < Api::BaseController
       :spoiler_text,
       :visibility,
       :scheduled_at,
+      :content_type,
       media_ids: [],
       poll: [
         :multiple,
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index eb7a0eb4a..3d98d583c 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -46,6 +46,7 @@ class Settings::PreferencesController < Settings::BaseController
       :setting_hide_followers_count,
       :setting_aggregate_reblogs,
       :setting_show_application,
+      :setting_default_content_type,
       notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account),
       interactions: %i(must_be_follower must_be_following)
     )
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index f117ce771..2fb97fa17 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -46,6 +46,7 @@ export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
 export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
+export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
 
 export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
 
@@ -147,6 +148,7 @@ export function submitCompose(routerHistory) {
     }
     api(getState).post('/api/v1/statuses', {
       status,
+      content_type: getState().getIn(['compose', 'content_type']),
       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
       media_ids: media.map(item => item.get('id')),
       sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
@@ -517,6 +519,13 @@ export function changeComposeVisibility(value) {
   };
 };
 
+export function changeComposeContentType(value) {
+  return {
+    type: COMPOSE_CONTENT_TYPE_CHANGE,
+    value,
+  };
+};
+
 export function insertEmojiCompose(position, emoji) {
   return {
     type: COMPOSE_EMOJI_INSERT,
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 550fe510f..7e22a7f98 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -71,11 +71,12 @@ export function fetchStatusFail(id, error, skipLoading) {
   };
 };
 
-export function redraft(status, raw_text) {
+export function redraft(status, raw_text, content_type) {
   return {
     type: REDRAFT,
     status,
     raw_text,
+    content_type,
   };
 };
 
@@ -94,7 +95,7 @@ export function deleteStatus(id, router, withRedraft = false) {
       dispatch(deleteFromTimelines(id));
 
       if (withRedraft) {
-        dispatch(redraft(status, response.data.text));
+        dispatch(redraft(status, response.data.text, response.data.content_type));
 
         if (!getState().getIn(['compose', 'mounted'])) {
           router.push('/statuses/new');
diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js
index cd7216de7..da93cc905 100644
--- a/app/javascript/flavours/glitch/containers/poll_container.js
+++ b/app/javascript/flavours/glitch/containers/poll_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import Poll from 'mastodon/components/poll';
+import Poll from 'flavours/glitch/components/poll';
 
 const mapStateToProps = (state, { pollId }) => ({
   poll: state.getIn(['polls', pollId]),
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js
index ee9730961..0c94f5514 100644
--- a/app/javascript/flavours/glitch/features/compose/components/options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/options.js
@@ -29,6 +29,10 @@ const messages = defineMessages({
     defaultMessage: 'Adjust status privacy',
     id: 'privacy.change',
   },
+  content_type: {
+    defaultMessage: 'Content type',
+    id: 'content-type.change',
+  },
   direct_long: {
     defaultMessage: 'Post to mentioned users only',
     id: 'privacy.direct.long',
@@ -41,6 +45,10 @@ const messages = defineMessages({
     defaultMessage: 'Draw something',
     id: 'compose.attach.doodle',
   },
+  html: {
+    defaultMessage: 'HTML',
+    id: 'compose.content-type.html',
+  },
   local_only_long: {
     defaultMessage: 'Do not post to other instances',
     id: 'advanced_options.local-only.long',
@@ -49,6 +57,14 @@ const messages = defineMessages({
     defaultMessage: 'Local-only',
     id: 'advanced_options.local-only.short',
   },
+  markdown: {
+    defaultMessage: 'Markdown',
+    id: 'compose.content-type.markdown',
+  },
+  plain: {
+    defaultMessage: 'Plain text',
+    id: 'compose.content-type.plain',
+  },
   private_long: {
     defaultMessage: 'Post to followers only',
     id: 'privacy.private.long',
@@ -113,6 +129,7 @@ class ComposerOptions extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     onChangeAdvancedOption: PropTypes.func,
     onChangeVisibility: PropTypes.func,
+    onChangeContentType: PropTypes.func,
     onTogglePoll: PropTypes.func,
     onDoodleOpen: PropTypes.func,
     onModalClose: PropTypes.func,
@@ -120,8 +137,10 @@ class ComposerOptions extends ImmutablePureComponent {
     onToggleSpoiler: PropTypes.func,
     onUpload: PropTypes.func,
     privacy: PropTypes.string,
+    contentType: PropTypes.string,
     resetFileKey: PropTypes.number,
     spoiler: PropTypes.bool,
+    showContentTypeChoice: PropTypes.bool,
   };
 
   //  Handles file selection.
@@ -162,6 +181,7 @@ class ComposerOptions extends ImmutablePureComponent {
     const {
       acceptContentTypes,
       advancedOptions,
+      contentType,
       disabled,
       allowMedia,
       hasMedia,
@@ -169,6 +189,7 @@ class ComposerOptions extends ImmutablePureComponent {
       hasPoll,
       intl,
       onChangeAdvancedOption,
+      onChangeContentType,
       onChangeVisibility,
       onTogglePoll,
       onModalClose,
@@ -177,6 +198,7 @@ class ComposerOptions extends ImmutablePureComponent {
       privacy,
       resetFileKey,
       spoiler,
+      showContentTypeChoice,
     } = this.props;
 
     //  We predefine our privacy items so that we can easily pick the
@@ -208,6 +230,24 @@ class ComposerOptions extends ImmutablePureComponent {
       },
     };
 
+    const contentTypeItems = {
+      plain: {
+        icon: 'align-left',
+        name: 'text/plain',
+        text: <FormattedMessage {...messages.plain} />,
+      },
+      html: {
+        icon: 'code',
+        name: 'text/html',
+        text: <FormattedMessage {...messages.html} />,
+      },
+      markdown: {
+        icon: 'arrow-circle-down',
+        name: 'text/markdown',
+        text: <FormattedMessage {...messages.markdown} />,
+      },
+    };
+
     //  The result.
     return (
       <div className='composer--options'>
@@ -272,6 +312,22 @@ class ComposerOptions extends ImmutablePureComponent {
           title={intl.formatMessage(messages.change_privacy)}
           value={privacy}
         />
+        {showContentTypeChoice && (
+          <Dropdown
+            disabled={disabled}
+            icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
+            items={[
+              contentTypeItems.plain,
+              contentTypeItems.html,
+              contentTypeItems.markdown,
+            ]}
+            onChange={onChangeContentType}
+            onModalClose={onModalClose}
+            onModalOpen={onModalOpen}
+            title={intl.formatMessage(messages.content_type)}
+            value={contentType}
+          />
+        )}
         {onToggleSpoiler && (
           <TextIconButton
             active={spoiler}
diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
index 2ac7ab8d8..c8c7ecd43 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js
@@ -2,8 +2,10 @@ import { connect } from 'react-redux';
 import Options from '../components/options';
 import {
   changeComposeAdvancedOption,
+  changeComposeContentType,
+  addPoll,
+  removePoll,
 } from 'flavours/glitch/actions/compose';
-import { addPoll, removePoll } from 'flavours/glitch/actions/compose';
 import { closeModal, openModal } from 'flavours/glitch/actions/modal';
 
 function mapStateToProps (state) {
@@ -17,6 +19,8 @@ function mapStateToProps (state) {
     allowMedia: !poll && (media ? media.size < 4 && !media.some(item => item.get('type') === 'video') : true),
     hasMedia: media && !!media.size,
     allowPoll: !(media && !!media.size),
+    showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
+    contentType: state.getIn(['compose', 'content_type']),
   };
 };
 
@@ -26,6 +30,10 @@ const mapDispatchToProps = (dispatch) => ({
     dispatch(changeComposeAdvancedOption(option, value));
   },
 
+  onChangeContentType(value) {
+    dispatch(changeComposeContentType(value));
+  },
+
   onTogglePoll() {
     dispatch((_, getState) => {
       if (getState().getIn(['compose', 'poll'])) {
diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js
index a7795a04d..e60eedfd9 100644
--- a/app/javascript/flavours/glitch/features/compose/index.js
+++ b/app/javascript/flavours/glitch/features/compose/index.js
@@ -29,7 +29,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 });
 
-export default @connect(mapStateToProps, mapDispatchToProps)
+export default @connect(mapStateToProps)
 @injectIntl
 class Compose extends React.PureComponent {
   static propTypes = {
diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js
index a13bffa3a..cd2d86713 100644
--- a/app/javascript/flavours/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -153,6 +153,14 @@ export default class LocalSettingsPage extends React.PureComponent {
         </LocalSettingsPageItem>
         <LocalSettingsPageItem
           settings={settings}
+          item={['show_content_type_choice']}
+          id='mastodon-settings--show_content_type_choice'
+          onChange={onChange}
+        >
+          <FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' />
+        </LocalSettingsPageItem>
+        <LocalSettingsPageItem
+          settings={settings}
           item={['side_arm']}
           id='mastodon-settings--side_arm'
           options={[
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index bc1785a48..c0c2fc547 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -25,6 +25,7 @@ import {
   COMPOSE_SPOILERNESS_CHANGE,
   COMPOSE_SPOILER_TEXT_CHANGE,
   COMPOSE_VISIBILITY_CHANGE,
+  COMPOSE_CONTENT_TYPE_CHANGE,
   COMPOSE_EMOJI_INSERT,
   COMPOSE_UPLOAD_CHANGE_REQUEST,
   COMPOSE_UPLOAD_CHANGE_SUCCESS,
@@ -44,7 +45,7 @@ import { REDRAFT } from 'flavours/glitch/actions/statuses';
 import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 import uuid from 'flavours/glitch/util/uuid';
 import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
-import { me } from 'flavours/glitch/util/initial_state';
+import { me, defaultContentType } from 'flavours/glitch/util/initial_state';
 import { overwrite } from 'flavours/glitch/util/js_helpers';
 import { unescapeHTML } from 'flavours/glitch/util/html';
 import { recoverHashtags } from 'flavours/glitch/util/hashtag';
@@ -66,6 +67,7 @@ const initialState = ImmutableMap({
   spoiler: false,
   spoiler_text: '',
   privacy: null,
+  content_type: defaultContentType || 'text/plain',
   text: '',
   focusDate: null,
   caretPosition: null,
@@ -141,6 +143,7 @@ function apiStatusToTextHashtags (state, status) {
 function clearAll(state) {
   return state.withMutations(map => {
     map.set('text', '');
+    if (defaultContentType) map.set('content_type', defaultContentType);
     map.set('spoiler', false);
     map.set('spoiler_text', '');
     map.set('is_submitting', false);
@@ -310,6 +313,10 @@ export default function compose(state = initialState, action) {
     return state
       .set('privacy', action.value)
       .set('idempotencyKey', uuid());
+  case COMPOSE_CONTENT_TYPE_CHANGE:
+    return state
+      .set('content_type', action.value)
+      .set('idempotencyKey', uuid());
   case COMPOSE_CHANGE:
     return state
       .set('text', action.text)
@@ -348,6 +355,7 @@ export default function compose(state = initialState, action) {
   case COMPOSE_RESET:
     return state.withMutations(map => {
       map.set('in_reply_to', null);
+      if (defaultContentType) map.set('content_type', defaultContentType);
       map.set('text', '');
       map.set('spoiler', false);
       map.set('spoiler_text', '');
@@ -427,6 +435,7 @@ export default function compose(state = initialState, action) {
   case REDRAFT:
     return state.withMutations(map => {
       map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status)));
+      map.set('content_type', action.content_type || 'text/plain');
       map.set('in_reply_to', action.status.get('in_reply_to_id'));
       map.set('privacy', action.status.get('visibility'));
       map.set('media_attachments', action.status.get('media_attachments'));
diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index 8dea4d8f6..5716c5982 100644
--- a/app/javascript/flavours/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -19,6 +19,7 @@ const initialState = ImmutableMap({
   preselect_on_reply: true,
   inline_preview_cards: true,
   hicolor_privacy_icons: false,
+  show_content_type_choice: false,
   content_warnings : ImmutableMap({
     auto_unfold : false,
     filter      : null,
diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index 62588eeaa..99d8a4dbc 100644
--- a/app/javascript/flavours/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
@@ -27,5 +27,6 @@ export const invitesEnabled = getMeta('invites_enabled');
 export const version = getMeta('version');
 export const mascot = getMeta('mascot');
 export const isStaff = getMeta('is_staff');
+export const defaultContentType = getMeta('default_content_type');
 
 export default initialState;
diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js
index 690f9ae5a..acab107a1 100644
--- a/app/javascript/mastodon/components/poll.js
+++ b/app/javascript/mastodon/components/poll.js
@@ -28,6 +28,7 @@ class Poll extends ImmutablePureComponent {
     intl: PropTypes.object.isRequired,
     dispatch: PropTypes.func,
     disabled: PropTypes.bool,
+    visible: PropTypes.bool,
   };
 
   state = {
@@ -69,13 +70,14 @@ class Poll extends ImmutablePureComponent {
   };
 
   renderOption (option, optionIndex) {
-    const { poll, disabled } = this.props;
-    const percent            = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
-    const leading            = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
-    const active             = !!this.state.selected[`${optionIndex}`];
-    const showResults        = poll.get('voted') || poll.get('expired');
+    const { poll, disabled, visible } = this.props;
+    const percent     = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
+    const leading     = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'));
+    const active      = !!this.state.selected[`${optionIndex}`];
+    const showResults = poll.get('voted') || poll.get('expired');
 
     let titleEmojified = option.get('title_emojified');
+
     if (!titleEmojified) {
       const emojiMap = makeEmojiMap(poll);
       titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
@@ -104,7 +106,7 @@ class Poll extends ImmutablePureComponent {
           {!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />}
           {showResults && <span className='poll__number'>{Math.round(percent)}%</span>}
 
-          <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
+          {visible ? <span dangerouslySetInnerHTML={{ __html: titleEmojified }} /> : <span>{String.fromCharCode(64 + optionIndex + 1)}</span>}
         </label>
       </li>
     );
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 42535ea68..6f66a4260 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -272,7 +272,7 @@ class Status extends ImmutablePureComponent {
     }
 
     if (status.get('poll')) {
-      media = <PollContainer pollId={status.get('poll')} />;
+      media = <PollContainer pollId={status.get('poll')} visible={!status.get('hidden')} />;
     } else if (status.get('media_attachments').size > 0) {
       if (this.props.muted) {
         media = (
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
index 93a468388..37a0e8845 100644
--- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js
+++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js
@@ -20,7 +20,7 @@ const mapStateToProps = state => ({
   focusDate: state.getIn(['compose', 'focusDate']),
   caretPosition: state.getIn(['compose', 'caretPosition']),
   preselectDate: state.getIn(['compose', 'preselectDate']),
-  is_submitting: state.getIn(['compose', 'is_submitting']),
+  isSubmitting: state.getIn(['compose', 'is_submitting']),
   isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
   isUploading: state.getIn(['compose', 'is_uploading']),
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 84471f9a3..059ecd979 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -106,7 +106,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
     }
 
     if (status.get('poll')) {
-      media = <PollContainer pollId={status.get('poll')} />;
+      media = <PollContainer pollId={status.get('poll')} visible={!status.get('hidden')} />;
     } else if (status.get('media_attachments').size > 0) {
       if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         const video = status.getIn(['media_attachments', 0]);
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 8a1aad41a..a099ff728 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -3,6 +3,27 @@
 require 'singleton'
 require_relative './sanitize_config'
 
+class HTMLRenderer < Redcarpet::Render::HTML
+  def block_code(code, language)
+    "<pre><code>#{encode(code).gsub("\n", "<br/>")}</code></pre>"
+  end
+
+  def autolink(link, link_type)
+    return link if link_type == :email
+    Formatter.instance.link_url(link)
+  end
+
+  private
+
+  def html_entities
+    @html_entities ||= HTMLEntities.new
+  end
+
+  def encode(html)
+    html_entities.encode(html)
+  end
+end
+
 class Formatter
   include Singleton
   include RoutingHelper
@@ -36,14 +57,25 @@ class Formatter
 
     html = raw_content
     html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
-    html = encode_and_link_urls(html, linkable_accounts)
+    html = format_markdown(html) if status.content_type == 'text/markdown'
+    html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type))
     html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify]
-    html = simple_format(html, {}, sanitize: false)
-    html = html.delete("\n")
+
+    if %w(text/markdown text/html).include?(status.content_type)
+      html = reformat(html)
+    else
+      html = simple_format(html, {}, sanitize: false)
+      html = html.delete("\n")
+    end
 
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
+  def format_markdown(html)
+    html = markdown_formatter.render(html)
+    html.delete("\r").delete("\n")
+  end
+
   def reformat(html)
     sanitize(html, Sanitize::Config::MASTODON_STRICT)
   end
@@ -98,8 +130,42 @@ class Formatter
     html.html_safe # rubocop:disable Rails/OutputSafety
   end
 
+  def link_url(url)
+    "<a href=\"#{encode(url)}\" target=\"blank\" rel=\"nofollow noopener\">#{link_html(url)}</a>"
+  end
+
   private
 
+  def markdown_formatter
+    return @markdown_formatter if defined?(@markdown_formatter)
+
+    extensions = {
+      autolink: true,
+      no_intra_emphasis: true,
+      fenced_code_blocks: true,
+      disable_indented_code_blocks: true,
+      strikethrough: true,
+      lax_spacing: true,
+      space_after_headers: true,
+      superscript: true,
+      underline: true,
+      highlight: true,
+      footnotes: false,
+    }
+
+    renderer = HTMLRenderer.new({
+      filter_html: false,
+      escape_html: false,
+      no_images: true,
+      no_styles: true,
+      safe_links_only: true,
+      hard_wrap: true,
+      link_attributes: { target: '_blank', rel: 'nofollow noopener' },
+    })
+
+    @markdown_formatter = Redcarpet::Markdown.new(renderer, extensions)
+  end
+
   def html_entities
     @html_entities ||= HTMLEntities.new
   end
@@ -109,14 +175,14 @@ class Formatter
   end
 
   def encode_and_link_urls(html, accounts = nil, options = {})
-    entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
-
     if accounts.is_a?(Hash)
       options  = accounts
       accounts = nil
     end
 
-    rewrite(html.dup, entities) do |entity|
+    entities = options[:keep_html] ? html_friendly_extractor(html) : utf8_friendly_extractor(html, extract_url_without_protocol: false)
+
+    rewrite(html.dup, entities, options[:keep_html]) do |entity|
       if entity[:url]
         link_to_url(entity, options)
       elsif entity[:hashtag]
@@ -186,7 +252,7 @@ class Formatter
     html
   end
 
-  def rewrite(text, entities)
+  def rewrite(text, entities, keep_html = false)
     text = text.to_s
 
     # Sort by start index
@@ -199,12 +265,12 @@ class Formatter
 
     last_index = entities.reduce(0) do |index, entity|
       indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
-      result << encode(text[index...indices.first])
+      result << (keep_html ? text[index...indices.first] : encode(text[index...indices.first]))
       result << yield(entity)
       indices.last
     end
 
-    result << encode(text[last_index..-1])
+    result << (keep_html ? text[last_index..-1] : encode(text[last_index..-1]))
 
     result.flatten.join
   end
@@ -247,6 +313,29 @@ class Formatter
     Extractor.remove_overlapping_entities(special + standard)
   end
 
+  def html_friendly_extractor(html, options = {})
+    gaps = []
+    total_offset = 0
+
+    escaped = html.gsub(/<[^>]*>/) do |match|
+      total_offset += match.length - 1
+      end_offset = Regexp.last_match.end(0)
+      gaps << [end_offset - total_offset, total_offset]
+      "\u200b"
+    end
+
+    entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) +
+               Extractor.extract_mentions_or_lists_with_indices(escaped)
+    Extractor.remove_overlapping_entities(entities).map do |extract|
+      pos = extract[:indices].first
+      offset_idx = gaps.rindex { |gap| gap.first <= pos }
+      offset = offset_idx.nil? ? 0 : gaps[offset_idx].last
+      next extract.merge(
+        :indices => [extract[:indices].first + offset, extract[:indices].last + offset]
+      )
+    end
+  end
+
   def link_to_url(entity, options = {})
     url        = Addressable::URI.parse(entity[:url])
     html_attrs = { target: '_blank', rel: 'nofollow noopener' }
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index 367ba9a83..802ca71fe 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -36,6 +36,7 @@ class UserSettingsDecorator
     user.settings['hide_network']        = hide_network_preference if change?('setting_hide_network')
     user.settings['aggregate_reblogs']   = aggregate_reblogs_preference if change?('setting_aggregate_reblogs')
     user.settings['show_application']    = show_application_preference if change?('setting_show_application')
+    user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type')
   end
 
   def merged_notification_emails
@@ -122,6 +123,10 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_aggregate_reblogs'
   end
 
+  def default_content_type_preference
+    settings['setting_default_content_type']
+  end
+
   def boolean_cast_setting(key)
     ActiveModel::Type::Boolean.new.cast(settings[key])
   end
diff --git a/app/models/status.rb b/app/models/status.rb
index 8736e65e3..6f3ba4cc3 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -24,6 +24,7 @@
 #  local_only             :boolean
 #  full_status_text       :text             default(""), not null
 #  poll_id                :bigint(8)
+#  content_type           :string
 #
 
 class Status < ApplicationRecord
@@ -74,6 +75,7 @@ class Status < ApplicationRecord
   validates_with DisallowedHashtagsValidator
   validates :reblog, uniqueness: { scope: :account }, if: :reblog?
   validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
+  validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true
 
   accepts_nested_attributes_for :poll
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 8985ebf53..496cb0b1b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -104,7 +104,7 @@ class User < ApplicationRecord
 
   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
            :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
-           :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, to: :settings, prefix: :setting, allow_nil: false
+           :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_content_type, to: :settings, prefix: :setting, allow_nil: false
 
   attr_reader :invite_code
   attr_writer :external
@@ -114,6 +114,10 @@ class User < ApplicationRecord
   end
 
   def invited?
+    invite_id.present?
+  end
+
+  def valid_invitation?
     invite_id.present? && invite.valid_for_use?
   end
 
@@ -274,7 +278,7 @@ class User < ApplicationRecord
   private
 
   def set_approved
-    self.approved = open_registrations? || invited? || external?
+    self.approved = open_registrations? || valid_invitation? || external?
   end
 
   def open_registrations?
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 850169af8..d74e56ebc 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -47,6 +47,7 @@ class InitialStateSerializer < ActiveModel::Serializer
       store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers
       store[:reduce_motion]   = object.current_account.user.setting_reduce_motion
       store[:is_staff]        = object.current_account.user.staff?
+      store[:default_content_type] = object.current_account.user.setting_default_content_type
     end
 
     store
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index 906f489db..b07937014 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -15,6 +15,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
 
   attribute :content, unless: :source_requested?
   attribute :text, if: :source_requested?
+  attribute :content_type, if: :source_requested?
 
   belongs_to :reblog, serializer: REST::StatusSerializer
   belongs_to :application, if: :show_application?
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index d2cca145b..c2584e090 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -168,6 +168,7 @@ class PostStatusService < BaseService
       visibility: @visibility,
       language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
       application: @options[:application],
+      content_type: @options[:content_type] || @account.user&.setting_default_content_type,
     }.compact
   end
 
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index deaa0549e..77ec52ab8 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -18,7 +18,9 @@ class ReblogService < BaseService
 
     return reblog unless reblog.nil?
 
-    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: options[:visibility] || account.user&.setting_default_privacy)
+    visibility = options[:visibility] || account.user&.setting_default_privacy
+    visibility = reblogged_status.visibility if reblogged_status.hidden?
+    reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
 
     DistributionWorker.perform_async(reblog.id)
 
diff --git a/app/validators/blacklisted_email_validator.rb b/app/validators/blacklisted_email_validator.rb
index a288c20ef..0d01a1c47 100644
--- a/app/validators/blacklisted_email_validator.rb
+++ b/app/validators/blacklisted_email_validator.rb
@@ -2,7 +2,7 @@
 
 class BlacklistedEmailValidator < ActiveModel::Validator
   def validate(user)
-    return if user.invited?
+    return if user.valid_invitation?
 
     @email = user.email
 
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index a50f33517..cd5bf9be2 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -25,6 +25,8 @@
   .fields-group
     = f.input :setting_default_privacy, collection: Status.selectable_visibilities, wrapper: :with_floating_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
 
+    = f.input :setting_default_content_type, collection: ['text/plain', 'text/markdown', 'text/html'], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_default_content_type_#{item.split('/')[1]}"), content_tag(:span, t("simple_form.hints.defaults.setting_default_content_type_#{item.split('/')[1]}"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
+
     = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label
 
   %hr#settings_other/
diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml
index e1b977da3..67f9274b6 100644
--- a/app/views/stream_entries/_og_image.html.haml
+++ b/app/views/stream_entries/_og_image.html.haml
@@ -7,6 +7,8 @@
       - unless media.file.meta.nil?
         = opengraph 'og:image:width', media.file.meta.dig('original', 'width')
         = opengraph 'og:image:height', media.file.meta.dig('original', 'height')
+      - if media.description.present?
+        = opengraph 'og:image:alt', media.description
     - elsif media.video? || media.gifv?
       - player_card = true
       = opengraph 'og:image', full_asset_url(media.file.url(:small))
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 22ee4b0ce..4927c5c51 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -293,8 +293,8 @@ co:
           one: Un contu tuccatu indè a database
           other: "%{count} conti tuccati indè a database"
         retroactive:
-          silence: Ùn silenzà più i conti nant’à stu duminiu
-          suspend: Ùn suspende più i conti nant’à stu duminiu
+          silence: Ùn silenzà più i conti affettati di stu duminiu
+          suspend: Ùn suspende più i conti affettati di stu duminiu
         title: Ùn bluccà più u duminiu %{domain}
         undo: Annullà
       undo: Annullà u blucchime di duminiu
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 5d05a13d6..9b73e3b1d 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -298,8 +298,8 @@ cs:
           one: Jeden účet v databázi byl ovlivněn
           other: "%{count} účtů v databázi bylo ovlivněno"
         retroactive:
-          silence: Odtišit všechny existující účty z této domény
-          suspend: Zrušit pozastavení všech existujících účtů z této domény
+          silence: Odtišit existující ovlivněné účty z této domény
+          suspend: Zrušit pozastavení existujících ovlivněných účtů z této domény
         title: Zrušit blokaci domény %{domain}
         undo: Odvolat
       undo: Odvolat blokaci domény
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index ba0e403e4..6fad7f73a 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -27,6 +27,9 @@ en:
         phrase: Will be matched regardless of casing in text or content warning of a toot
         scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
         setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts)
+        setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise
+        setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise
+        setting_default_content_type_plain: When writing toots, assume they are plain text with no special formatting, unless specified otherwise (default Mastodon behavior)
         setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
         setting_display_media_default: Hide media marked as sensitive
         setting_display_media_hide_all: Always hide all media
@@ -93,6 +96,10 @@ en:
         setting_aggregate_reblogs: Group boosts in timelines
         setting_auto_play_gif: Auto-play animated GIFs
         setting_boost_modal: Show confirmation dialog before boosting
+        setting_default_content_type: Default format for toots
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: Plain text
         setting_default_language: Posting language
         setting_default_privacy: Post privacy
         setting_default_sensitive: Always mark media as sensitive
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 51795849c..ff0d69fb0 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -163,7 +163,7 @@ sk:
       resubscribe: Znovu odoberaj
       role: Oprávnenia
       roles:
-        admin: Administrátor
+        admin: Správca
         moderator: Moderátor
         staff: Člen
         user: Užívateľ
@@ -174,7 +174,7 @@ sk:
         created_reports: Vytvorené hlásenia
         targeted_reports: Nahlásenia od ostatných
       silence: Stíš
-      silenced: Utíšený/é
+      silenced: Stíšený/é
       statuses: Príspevky
       subscribe: Odoberaj
       suspended: Zablokovaní
@@ -294,12 +294,12 @@ sk:
         suspend: vylúčený
       show:
         affected_accounts:
-          few: "%{count} účty v databáze ovplyvnených"
-          one: Jeden účet v databáze bol ovplyvnený
-          other: "%{count} účtov v databáze bolo ovplyvnených"
+          few: Je ovplyvnených %{count} účtov v databázi
+          one: Jeden účet v databázi bol ovplyvnený
+          other: "%{count} účty v databáze boli ovplyvnené"
         retroactive:
-          silence: Zruš stíšenie všetkých existujúcich účtov z tejto domény
-          suspend: Zruš suspendáciu všetkých existujúcich účtov z tejto domény
+          silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény
+          suspend: Zruš suspendáciu všetkých momentálne ovplyvnených účtov z tejto domény
         title: Zruš blokovanie domény %{domain}
         undo: Vráť späť
       undo: Odvolaj blokovanie domény
@@ -396,7 +396,7 @@ sk:
         desc_html: Sčítanie miestne uverejnených príspevkov, aktívnych užívateľov, a nových registrácii, v týždenných intervaloch
         title: Vydať hromadné štatistiky o užívateľskej aktivite
       bootstrap_timeline_accounts:
-        desc_html: Ak je prezývok viacero, každú oddeľte čiarkou. Možno zadať iba miestne, odomknuté účty. Pokiaľ necháte prázdne, je to pre všetkých miestnych administrátorov.
+        desc_html: Ak je prezývok viacero, každú oddeľ čiarkou. Je možné zadať iba miestne, odomknuté účty. Pokiaľ necháš prázdne, je to pre všetkých miestnych správcov.
         title: Štandardní následovníci nových užívateľov
       contact_information:
         email: Pracovný email
@@ -405,30 +405,30 @@ sk:
         desc_html: Uprav vzhľad pomocou CSS, ktoré je načítané na každej stránke
         title: Vlastné CSS
       hero:
-        desc_html: Zobrazuje sa na hlavnej stránke. Doporučuje sa rozlišenie aspoň 600x100px Pokiaľ nič nieje dodané, bude nastavený základný orázok serveru
+        desc_html: Zobrazuje sa na hlavnej stránke. Doporučené je rozlišenie aspoň 600x100px. Pokiaľ nič nieje dodané, bude nastavený základný orázok serveru.
         title: Obrázok hrdinu
       mascot:
-        desc_html: Zobrazované na viacerých stránkach. Odporúčaná veľkosť aspoň 293×205px. Pokiaľ nieje nahraté, bude zobrazený základný maskot
+        desc_html: Zobrazované na viacerých stránkach. Odporúčaná veľkosť aspoň 293×205px. Pokiaľ nieje nahraté, bude zobrazený základný maskot.
         title: Obrázok maskota
       peers_api_enabled:
-        desc_html: Domény, na ktoré tento server už v rámci fediverse natrafil
+        desc_html: Domény, na ktoré tento server už v rámci fediversa natrafil
         title: Zverejni zoznam objavených serverov
       preview_sensitive_media:
-        desc_html: Náhľad odkazov z iných serverov, bude zobrazený aj vtedy, keď sú médiá označené ako senzitívne
+        desc_html: Náhľad odkazov z iných serverov, bude zobrazený aj vtedy, keď sú médiá označené ako citlivé
         title: Ukazuj aj chúlostivé médiá v náhľadoch OpenGraph
       profile_directory:
         desc_html: Povoľ užívateľom, aby mohli byť nájdení
         title: Zapni profilový katalóg
       registrations:
         closed_message:
-          desc_html: Toto sa zobrazí na hlavnej stránke v prípade že sú registrácie uzavreté. Možno tu použiť aj HTML kód
+          desc_html: Toto sa zobrazí na hlavnej stránke v prípade, že sú registrácie uzavreté. Možno tu použiť aj HTML kód
           title: Správa o uzavretých registráciách
         deletion:
-          desc_html: Dovoľiť každému aby si mohli zmazať svok účet
-          title: Sprístupniť možnosť vymazať si účet
+          desc_html: Dovoľ každému aby si mohli zmazať svok účet
+          title: Sprístupni možnosť vymazať si účet
         min_invite_role:
           disabled: Nikto
-          title: Povoliť pozvánky od
+          title: Povoľ pozvánky od
       registrations_mode:
         modes:
           approved: Pre registráciu je nutné povolenie
@@ -436,11 +436,11 @@ sk:
           open: Ktokoľvek sa môže zaregistrovať
         title: Režím registrácií
       show_known_fediverse_at_about_page:
-        desc_html: Pokiaľ je zapnuté, bude v ukážke osi možné nahliadnúť príspevky z celého známeho fediversa. Inak budú ukázané iba príspevky z miestnej osi.
+        desc_html: Ak je zapnuté, bude v ukážke osi možné nahliadnúť príspevky z celého známeho fediversa. Inak budú ukázané iba príspevky z miestnej osi.
         title: Ukáž celé známe fediverse na náhľade osi
       show_staff_badge:
-        desc_html: Zobraz moderátorsky odznak na užívateľovom profile
-        title: Zobraz značku moderátora
+        desc_html: Ukáž moderátorsky odznak na užívateľovom profile
+        title: Ukáž značku moderátora
       site_description:
         desc_html: Oboznamujúci paragraf na hlavnej stránke a pri meta tagoch. Opíš, čo robí tento Mastodon server špecifickým, a ďalej hocičo iné, čo považuješ za dôležité. Môžeš použiť HTML kód, hlavne <code>&lt;a&gt;</code> a <code>&lt;em&gt;</code>.
         title: Popis servera
diff --git a/config/settings.yml b/config/settings.yml
index c3aeab551..69996af25 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -64,6 +64,7 @@ defaults: &defaults
   show_known_fediverse_at_about_page: true
   show_reblogs_in_public_timelines: false
   show_replies_in_public_timelines: false
+  default_content_type: 'text/plain'
 
 development:
   <<: *defaults
diff --git a/db/migrate/20190512200918_add_content_type_to_statuses.rb b/db/migrate/20190512200918_add_content_type_to_statuses.rb
new file mode 100644
index 000000000..efbe2caa7
--- /dev/null
+++ b/db/migrate/20190512200918_add_content_type_to_statuses.rb
@@ -0,0 +1,5 @@
+class AddContentTypeToStatuses < ActiveRecord::Migration[5.2]
+  def change
+    add_column :statuses, :content_type, :string
+  end
+end
diff --git a/db/post_migrate/20190519130537_remove_boosts_widening_audience.rb b/db/post_migrate/20190519130537_remove_boosts_widening_audience.rb
new file mode 100644
index 000000000..d2d924239
--- /dev/null
+++ b/db/post_migrate/20190519130537_remove_boosts_widening_audience.rb
@@ -0,0 +1,23 @@
+class RemoveBoostsWideningAudience < ActiveRecord::Migration[5.2]
+  disable_ddl_transaction!
+
+  def up
+    public_boosts = Status.find_by_sql(<<-SQL)
+      SELECT boost.id
+      FROM statuses AS boost
+      LEFT JOIN statuses AS boosted ON boost.reblog_of_id = boosted.id
+      WHERE
+        boost.id > 101746055577600000
+        AND (boost.local = TRUE OR boost.uri IS NULL)
+        AND boost.visibility IN (0, 1)
+        AND boost.reblog_of_id IS NOT NULL
+        AND boosted.visibility = 2
+    SQL
+
+    RemovalWorker.push_bulk(public_boosts.pluck(:id))
+  end
+
+  def down
+    raise ActiveRecord::IrreversibleMigration
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 75a67c6c3..96961c812 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_05_11_152737) do
+ActiveRecord::Schema.define(version: 2019_05_19_130537) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -637,6 +637,7 @@ ActiveRecord::Schema.define(version: 2019_05_11_152737) do
     t.bigint "in_reply_to_account_id"
     t.boolean "local_only"
     t.bigint "poll_id"
+    t.string "content_type"
     t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
     t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
diff --git a/docker-compose.yml b/docker-compose.yml
index 47662d470..93d47f1a0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,7 +13,7 @@ services:
 
   redis:
     restart: always
-    image: redis:4.0-alpine
+    image: redis:5.0-alpine
     networks:
       - internal_network
     healthcheck:
diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb
index 9e66c6643..9d84c41d5 100644
--- a/spec/services/reblog_service_spec.rb
+++ b/spec/services/reblog_service_spec.rb
@@ -4,10 +4,9 @@ RSpec.describe ReblogService, type: :service do
   let(:alice)  { Fabricate(:account, username: 'alice') }
 
   context 'creates a reblog with appropriate visibility' do
-    let(:bob)               { Fabricate(:account, username: 'bob') }
     let(:visibility)        { :public }
     let(:reblog_visibility) { :public }
-    let(:status)            { Fabricate(:status, account: bob, visibility: visibility) }
+    let(:status)            { Fabricate(:status, account: alice, visibility: visibility) }
 
     subject { ReblogService.new }
 
@@ -22,6 +21,15 @@ RSpec.describe ReblogService, type: :service do
         expect(status.reblogs.first.visibility).to eq 'private'
       end
     end
+
+    describe 'public reblogs of private toots should remain private' do
+      let(:visibility)        { :private }
+      let(:reblog_visibility) { :public }
+
+      it 'reblogs privately' do
+        expect(status.reblogs.first.visibility).to eq 'private'
+      end
+    end
   end
 
   context 'OStatus' do
diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb
index 84b0107dd..ccc5dc0f4 100644
--- a/spec/validators/blacklisted_email_validator_spec.rb
+++ b/spec/validators/blacklisted_email_validator_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
     let(:errors) { double(add: nil) }
 
     before do
-      allow(user).to receive(:invited?) { false }
+      allow(user).to receive(:valid_invitation?) { false }
       allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
       described_class.new.validate(user)
     end