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/web/settings_controller.rb9
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/instance_actors_controller.rb2
-rw-r--r--app/helpers/mascot_helper.rb13
-rw-r--r--app/javascript/mastodon/actions/boosts.js29
-rw-r--r--app/javascript/mastodon/actions/interactions.js4
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js1
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js18
-rw-r--r--app/javascript/mastodon/containers/dropdown_menu_container.js1
-rw-r--r--app/javascript/mastodon/containers/status_container.js7
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js18
-rw-r--r--app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js1
-rw-r--r--app/javascript/mastodon/features/notifications/containers/notification_container.js7
-rw-r--r--app/javascript/mastodon/features/picture_in_picture/components/footer.js11
-rw-r--r--app/javascript/mastodon/features/status/components/action_bar.js7
-rw-r--r--app/javascript/mastodon/features/status/containers/detailed_status_container.js7
-rw-r--r--app/javascript/mastodon/features/status/index.js7
-rw-r--r--app/javascript/mastodon/features/ui/components/boost_modal.js38
-rw-r--r--app/javascript/mastodon/reducers/boosts.js25
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/modal.js3
-rw-r--r--app/javascript/mastodon/utils/resize_image.js2
-rw-r--r--app/javascript/styles/mastodon/components.scss10
-rw-r--r--app/javascript/styles/mastodon/modal.scss13
-rw-r--r--app/lib/activitypub/activity/follow.rb13
-rw-r--r--app/lib/webfinger.rb12
-rw-r--r--app/models/concerns/account_finder_concern.rb2
-rw-r--r--app/models/media_attachment.rb2
-rw-r--r--app/services/fetch_oembed_service.rb12
-rw-r--r--app/views/layouts/modal.html.haml1
30 files changed, 222 insertions, 57 deletions
diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb
index 3d65e46ed..601e25e6e 100644
--- a/app/controllers/api/web/settings_controller.rb
+++ b/app/controllers/api/web/settings_controller.rb
@@ -2,17 +2,16 @@
 
 class Api::Web::SettingsController < Api::Web::BaseController
   before_action :require_user!
+  before_action :set_setting
 
   def update
-    setting.data = params[:data]
-    setting.save!
-
+    @setting.update!(data: params[:data])
     render_empty
   end
 
   private
 
-  def setting
-    @_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
+  def set_setting
+    @setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 41fe9d88a..a4b740b89 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -44,7 +44,7 @@ class ApplicationController < ActionController::Base
   private
 
   def https_enabled?
-    Rails.env.production? && !request.path.start_with?('/health')
+    Rails.env.production? && !request.path.start_with?('/health') && !request.headers["Host"].ends_with?(".onion")
   end
 
   def authorized_fetch_mode?
diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb
index 4b074ca19..b3b5476e2 100644
--- a/app/controllers/instance_actors_controller.rb
+++ b/app/controllers/instance_actors_controller.rb
@@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
   private
 
   def set_account
-    @account = Account.find(-99)
+    @account = Account.representative
   end
 
   def restrict_fields_to
diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb
new file mode 100644
index 000000000..0124c74f1
--- /dev/null
+++ b/app/helpers/mascot_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module MascotHelper
+  def mascot_url
+    full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
+  end
+
+  private
+
+  def instance_presenter
+    @instance_presenter ||= InstancePresenter.new
+  end
+end
diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js
new file mode 100644
index 000000000..6e14065d6
--- /dev/null
+++ b/app/javascript/mastodon/actions/boosts.js
@@ -0,0 +1,29 @@
+import { openModal } from './modal';
+
+export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
+export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
+
+export function initBoostModal(props) {
+  return (dispatch, getState) => {
+    const default_privacy = getState().getIn(['compose', 'default_privacy']);
+
+    const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
+
+    dispatch({
+      type: BOOSTS_INIT_MODAL,
+      privacy
+    });
+
+    dispatch(openModal('BOOST', props));
+  };
+}
+
+
+export function changeBoostPrivacy(privacy) {
+  return dispatch => {
+    dispatch({
+      type: BOOSTS_CHANGE_PRIVACY,
+      privacy,
+    });
+  };
+}
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 28c6b1a62..d60ccc1fb 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
 export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
 export const UNBOOKMARK_FAIL    = 'UNBOOKMARKED_FAIL';
 
-export function reblog(status) {
+export function reblog(status, visibility) {
   return function (dispatch, getState) {
     dispatch(reblogRequest(status));
 
-    api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
+    api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
       // The reblog API method returns a new status wrapped around the original. In this case we are only
       // interested in how the original is modified, hence passing it skipping the wrapper
       dispatch(importFetchedStatus(response.data.reblog));
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index c6b4b1187..7d0588901 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent {
     disabled: PropTypes.bool,
     status: ImmutablePropTypes.map,
     isUserTouching: PropTypes.func,
-    isModalOpen: PropTypes.bool.isRequired,
     onOpen: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
     dropdownPlacement: PropTypes.string,
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 66b5a17ac..9981f2449 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -223,10 +223,11 @@ class StatusActionBar extends ImmutablePureComponent {
   render () {
     const { status, relationship, intl, withDismiss, scrollKey } = this.props;
 
-    const mutingConversation = status.get('muted');
     const anonymousAccess    = !me;
     const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
+    const mutingConversation = status.get('muted');
     const account            = status.get('account');
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
 
     let menu = [];
 
@@ -237,19 +238,22 @@ class StatusActionBar extends ImmutablePureComponent {
       menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
     }
 
+    menu.push(null);
+
     menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
+
+    if (writtenByMe && publicStatus) {
+      menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+    }
+
     menu.push(null);
 
-    if (status.getIn(['account', 'id']) === me || withDismiss) {
+    if (writtenByMe || withDismiss) {
       menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
       menu.push(null);
     }
 
-    if (status.getIn(['account', 'id']) === me) {
-      if (publicStatus) {
-        menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
-      }
-
+    if (writtenByMe) {
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
       menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
     } else {
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js
index 6ec9bbffd..c45bab40b 100644
--- a/app/javascript/mastodon/containers/dropdown_menu_container.js
+++ b/app/javascript/mastodon/containers/dropdown_menu_container.js
@@ -6,7 +6,6 @@ import DropdownMenu from '../components/dropdown_menu';
 import { isUserTouching } from '../is_mobile';
 
 const mapStateToProps = state => ({
-  isModalOpen: state.get('modal').modalType === 'ACTIONS',
   dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
   openDropdownId: state.getIn(['dropdown_menu', 'openId']),
   openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index d6bcb8973..9abdec138 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -35,6 +35,7 @@ import {
 } from '../actions/domain_blocks';
 import { initMuteModal } from '../actions/mutes';
 import { initBlockModal } from '../actions/blocks';
+import { initBoostModal } from '../actions/boosts';
 import { initReport } from '../actions/reports';
 import { openModal } from '../actions/modal';
 import { deployPictureInPicture } from '../actions/picture_in_picture';
@@ -82,11 +83,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     });
   },
 
-  onModalReblog (status) {
+  onModalReblog (status, privacy) {
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else {
-      dispatch(reblog(status));
+      dispatch(reblog(status, privacy));
     }
   },
 
@@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     if ((e && e.shiftKey) || !boostModal) {
       this.onModalReblog(status);
     } else {
-      dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+      dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
     }
   },
 
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 309f46290..df59f46b3 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -127,7 +127,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
           // It should not be transformed when mounting because the resulting
           // size will be used to determine the coordinate of the menu by
           // react-overlays
-          <div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, zIndex: 2 }} role='listbox' ref={this.setRef}>
+          <div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
             {items.map(item => (
               <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
                 <div className='privacy-dropdown__option__icon'>
@@ -153,11 +153,12 @@ class PrivacyDropdown extends React.PureComponent {
 
   static propTypes = {
     isUserTouching: PropTypes.func,
-    isModalOpen: PropTypes.bool.isRequired,
     onModalOpen: PropTypes.func,
     onModalClose: PropTypes.func,
     value: PropTypes.string.isRequired,
     onChange: PropTypes.func.isRequired,
+    noDirect: PropTypes.bool,
+    container: PropTypes.func,
     intl: PropTypes.object.isRequired,
   };
 
@@ -167,7 +168,7 @@ class PrivacyDropdown extends React.PureComponent {
   };
 
   handleToggle = ({ target }) => {
-    if (this.props.isUserTouching()) {
+    if (this.props.isUserTouching && this.props.isUserTouching()) {
       if (this.state.open) {
         this.props.onModalClose();
       } else {
@@ -236,12 +237,17 @@ class PrivacyDropdown extends React.PureComponent {
       { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
       { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
       { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
-      { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
     ];
+
+    if (!this.props.noDirect) {
+      this.options.push(
+        { icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
+      );
+    }
   }
 
   render () {
-    const { value, intl } = this.props;
+    const { value, container, intl } = this.props;
     const { open, placement } = this.state;
 
     const valueOption = this.options.find(item => item.value === value);
@@ -264,7 +270,7 @@ class PrivacyDropdown extends React.PureComponent {
           />
         </div>
 
-        <Overlay show={open} placement={placement} target={this}>
+        <Overlay show={open} placement={placement} target={this} container={container}>
           <PrivacyDropdownMenu
             items={this.options}
             value={value}
diff --git a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
index 0ddf531d3..e5bbb94da 100644
--- a/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
@@ -5,7 +5,6 @@ import { openModal, closeModal } from '../../../actions/modal';
 import { isUserTouching } from '../../../is_mobile';
 
 const mapStateToProps = state => ({
-  isModalOpen: state.get('modal').modalType === 'ACTIONS',
   value: state.getIn(['compose', 'privacy']),
 });
 
diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js
index 78576c760..555d5e1b5 100644
--- a/app/javascript/mastodon/features/notifications/containers/notification_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js
@@ -1,6 +1,7 @@
 import { connect } from 'react-redux';
 import { makeGetNotification, makeGetStatus } from '../../../selectors';
 import Notification from '../components/notification';
+import { initBoostModal } from '../../../actions/boosts';
 import { openModal } from '../../../actions/modal';
 import { mentionCompose } from '../../../actions/compose';
 import {
@@ -35,8 +36,8 @@ const mapDispatchToProps = dispatch => ({
     dispatch(mentionCompose(account, router));
   },
 
-  onModalReblog (status) {
-    dispatch(reblog(status));
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
   },
 
   onReblog (status, e) {
@@ -46,7 +47,7 @@ const mapDispatchToProps = dispatch => ({
       if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
       }
     }
   },
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 1b1ec6d54..1ecb18bf8 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -10,6 +10,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import { replyCompose } from 'mastodon/actions/compose';
 import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
 import { makeGetStatus } from 'mastodon/selectors';
+import { initBoostModal } from 'mastodon/actions/boosts';
 import { openModal } from 'mastodon/actions/modal';
 
 const messages = defineMessages({
@@ -89,9 +90,9 @@ class Footer extends ImmutablePureComponent {
     }
   };
 
-  _performReblog = () => {
-    const { dispatch, status } = this.props;
-    dispatch(reblog(status));
+  _performReblog = (status, privacy) => {
+    const { dispatch } = this.props;
+    dispatch(reblog(status, privacy));
   }
 
   handleReblogClick = e => {
@@ -100,9 +101,9 @@ class Footer extends ImmutablePureComponent {
     if (status.get('reblogged')) {
       dispatch(unreblog(status));
     } else if ((e && e.shiftKey) || !boostModal) {
-      this._performReblog();
+      this._performReblog(status);
     } else {
-      dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
+      dispatch(initBoostModal({ status, onReblog: this._performReblog }));
     }
   };
 
diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js
index d7d504bc5..ffa2510c0 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.js
+++ b/app/javascript/mastodon/features/status/components/action_bar.js
@@ -187,9 +187,10 @@ class ActionBar extends React.PureComponent {
   render () {
     const { status, relationship, intl } = this.props;
 
-    const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
+    const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility'));
     const mutingConversation = status.get('muted');
     const account            = status.get('account');
+    const writtenByMe        = status.getIn(['account', 'id']) === me;
 
     let menu = [];
 
@@ -199,12 +200,12 @@ class ActionBar extends React.PureComponent {
       menu.push(null);
     }
 
-    if (me === status.getIn(['account', 'id'])) {
+    if (writtenByMe) {
       if (publicStatus) {
         menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+        menu.push(null);
       }
 
-      menu.push(null);
       menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
       menu.push(null);
       menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 0ac4519c8..bfed16620 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -23,6 +23,7 @@ import {
 } from '../../../actions/statuses';
 import { initMuteModal } from '../../../actions/mutes';
 import { initBlockModal } from '../../../actions/blocks';
+import { initBoostModal } from '../../../actions/boosts';
 import { initReport } from '../../../actions/reports';
 import { openModal } from '../../../actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
@@ -68,8 +69,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     });
   },
 
-  onModalReblog (status) {
-    dispatch(reblog(status));
+  onModalReblog (status, privacy) {
+    dispatch(reblog(status, privacy));
   },
 
   onReblog (status, e) {
@@ -79,7 +80,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
       if (e.shiftKey || !boostModal) {
         this.onModalReblog(status);
       } else {
-        dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
+        dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
       }
     }
   },
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 09822f372..df8362a1b 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -42,6 +42,7 @@ import {
 } from '../../actions/domain_blocks';
 import { initMuteModal } from '../../actions/mutes';
 import { initBlockModal } from '../../actions/blocks';
+import { initBoostModal } from '../../actions/boosts';
 import { initReport } from '../../actions/reports';
 import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
@@ -234,8 +235,8 @@ class Status extends ImmutablePureComponent {
     }
   }
 
-  handleModalReblog = (status) => {
-    this.props.dispatch(reblog(status));
+  handleModalReblog = (status, privacy) => {
+    this.props.dispatch(reblog(status, privacy));
   }
 
   handleReblogClick = (status, e) => {
@@ -245,7 +246,7 @@ class Status extends ImmutablePureComponent {
       if ((e && e.shiftKey) || !boostModal) {
         this.handleModalReblog(status);
       } else {
-        this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
+        this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
       }
     }
   }
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js
index 963bb5dc4..83229833b 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.js
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.js
@@ -1,4 +1,5 @@
 import React from 'react';
+import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@@ -10,7 +11,9 @@ import DisplayName from '../../../components/display_name';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Icon from 'mastodon/components/icon';
 import AttachmentList from 'mastodon/components/attachment_list';
+import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
 import classNames from 'classnames';
+import { changeBoostPrivacy } from 'mastodon/actions/boosts';
 
 const messages = defineMessages({
   cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@@ -21,7 +24,22 @@ const messages = defineMessages({
   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
 });
 
-export default @injectIntl
+const mapStateToProps = state => {
+  return {
+    privacy: state.getIn(['boosts', 'new', 'privacy']),
+  };
+};
+
+const mapDispatchToProps = dispatch => {
+  return {
+    onChangeBoostPrivacy(value) {
+      dispatch(changeBoostPrivacy(value));
+    },
+  };
+};
+
+export default @connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
 class BoostModal extends ImmutablePureComponent {
 
   static contextTypes = {
@@ -32,6 +50,8 @@ class BoostModal extends ImmutablePureComponent {
     status: ImmutablePropTypes.map.isRequired,
     onReblog: PropTypes.func.isRequired,
     onClose: PropTypes.func.isRequired,
+    onChangeBoostPrivacy: PropTypes.func.isRequired,
+    privacy: PropTypes.string.isRequired,
     intl: PropTypes.object.isRequired,
   };
 
@@ -40,7 +60,7 @@ class BoostModal extends ImmutablePureComponent {
   }
 
   handleReblog = () => {
-    this.props.onReblog(this.props.status);
+    this.props.onReblog(this.props.status, this.props.privacy);
     this.props.onClose();
   }
 
@@ -52,12 +72,16 @@ class BoostModal extends ImmutablePureComponent {
     }
   }
 
+  _findContainer = () => {
+    return document.getElementsByClassName('modal-root__container')[0];
+  };
+
   setRef = (c) => {
     this.button = c;
   }
 
   render () {
-    const { status, intl } = this.props;
+    const { status, privacy, intl } = this.props;
     const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
 
     const visibilityIconInfo = {
@@ -102,6 +126,14 @@ class BoostModal extends ImmutablePureComponent {
 
         <div className='boost-modal__action-bar'>
           <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
+          {status.get('visibility') !== 'private' && !status.get('reblogged') && (
+            <PrivacyDropdown
+              noDirect
+              value={privacy}
+              container={this._findContainer}
+              onChange={this.props.onChangeBoostPrivacy}
+            />
+          )}
           <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
         </div>
       </div>
diff --git a/app/javascript/mastodon/reducers/boosts.js b/app/javascript/mastodon/reducers/boosts.js
new file mode 100644
index 000000000..d0d825057
--- /dev/null
+++ b/app/javascript/mastodon/reducers/boosts.js
@@ -0,0 +1,25 @@
+import Immutable from 'immutable';
+
+import {
+  BOOSTS_INIT_MODAL,
+  BOOSTS_CHANGE_PRIVACY,
+} from 'mastodon/actions/boosts';
+
+const initialState = Immutable.Map({
+  new: Immutable.Map({
+    privacy: 'public',
+  }),
+});
+
+export default function mutes(state = initialState, action) {
+  switch (action.type) {
+  case BOOSTS_INIT_MODAL:
+    return state.withMutations((state) => {
+      state.setIn(['new', 'privacy'], action.privacy);
+    });
+  case BOOSTS_CHANGE_PRIVACY:
+    return state.setIn(['new', 'privacy'], action.privacy);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a8fb69c27..3b3c5ae29 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -16,6 +16,7 @@ import push_notifications from './push_notifications';
 import status_lists from './status_lists';
 import mutes from './mutes';
 import blocks from './blocks';
+import boosts from './boosts';
 import reports from './reports';
 import contexts from './contexts';
 import compose from './compose';
@@ -57,6 +58,7 @@ const reducers = {
   push_notifications,
   mutes,
   blocks,
+  boosts,
   reports,
   contexts,
   compose,
diff --git a/app/javascript/mastodon/reducers/modal.js b/app/javascript/mastodon/reducers/modal.js
index a30da2db1..cb53887c7 100644
--- a/app/javascript/mastodon/reducers/modal.js
+++ b/app/javascript/mastodon/reducers/modal.js
@@ -1,4 +1,5 @@
 import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
+import { TIMELINE_DELETE } from '../actions/timelines';
 
 const initialState = {
   modalType: null,
@@ -11,6 +12,8 @@ export default function modal(state = initialState, action) {
     return { modalType: action.modalType, modalProps: action.modalProps };
   case MODAL_CLOSE:
     return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
+  case TIMELINE_DELETE:
+    return (state.modalProps.statusId === action.id) ? initialState : state;
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
index 8f1485379..22ff86801 100644
--- a/app/javascript/mastodon/utils/resize_image.js
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -1,6 +1,6 @@
 import EXIF from 'exif-js';
 
-const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
+const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
 
 const _browser_quirks = {};
 
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index c9a2e75b0..f3ce1595a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4209,6 +4209,7 @@ a.status-card.compact:hover {
   border-radius: 4px;
   margin-left: 40px;
   overflow: hidden;
+  z-index: 2;
 
   &.top {
     transform-origin: 50% 100%;
@@ -4219,6 +4220,15 @@ a.status-card.compact:hover {
   }
 }
 
+.modal-root__container .privacy-dropdown {
+  flex-grow: 0;
+}
+
+.modal-root__container .privacy-dropdown__dropdown {
+  pointer-events: auto;
+  z-index: 9999;
+}
+
 .privacy-dropdown__option {
   color: $inverted-text-color;
   padding: 10px;
diff --git a/app/javascript/styles/mastodon/modal.scss b/app/javascript/styles/mastodon/modal.scss
index 10de454c6..6c6de4206 100644
--- a/app/javascript/styles/mastodon/modal.scss
+++ b/app/javascript/styles/mastodon/modal.scss
@@ -12,10 +12,19 @@
   flex-direction: column;
   justify-content: flex-end;
 
-  > * {
+  > div {
     flex: 1;
     max-height: 235px;
-    background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain;
+    position: relative;
+
+    img {
+      max-height: 100%;
+      max-width: 100%;
+      height: 100%;
+      position: absolute;
+      bottom: 0;
+      left: 0;
+    }
   }
 }
 
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 0beec68ab..4efb84b8c 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -6,7 +6,14 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
   def perform
     target_account = account_from_uri(object_uri)
 
-    return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
+    return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id'])
+
+    # Update id of already-existing follow requests
+    existing_follow_request = ::FollowRequest.find_by(account: @account, target_account: target_account)
+    unless existing_follow_request.nil?
+      existing_follow_request.update!(uri: @json['id'])
+      return
+    end
 
     if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
       reject_follow_request!(target_account)
@@ -14,7 +21,9 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
     end
 
     # Fast-forward repeat follow requests
-    if @account.following?(target_account)
+    existing_follow = ::Follow.find_by(account: @account, target_account: target_account)
+    unless existing_follow.nil?
+      existing_follow.update!(uri: @json['id'])
       AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
       return
     end
diff --git a/app/lib/webfinger.rb b/app/lib/webfinger.rb
index 702365939..40795a7aa 100644
--- a/app/lib/webfinger.rb
+++ b/app/lib/webfinger.rb
@@ -88,10 +88,18 @@ class Webfinger
   end
 
   def standard_url
-    "https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
+    if @domain.ends_with? ".onion"
+      "http://#{@domain}/.well-known/webfinger?resource=#{@uri}"
+    else
+      "https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
+    end
   end
 
   def host_meta_url
-    "https://#{@domain}/.well-known/host-meta"
+    if @domain.ends_with? ".onion"
+      "http://#{@domain}/.well-known/host-meta"
+    else
+      "https://#{@domain}/.well-known/host-meta"
+    end
   end
 end
diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb
index 04b2c981b..0dadddad1 100644
--- a/app/models/concerns/account_finder_concern.rb
+++ b/app/models/concerns/account_finder_concern.rb
@@ -14,6 +14,8 @@ module AccountFinderConcern
 
     def representative
       Account.find(-99)
+    rescue ActiveRecord::RecordNotFound
+      Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
     end
 
     def find_local(username)
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index cc81b648c..96386d841 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -59,7 +59,7 @@ class MediaAttachment < ApplicationRecord
 
   IMAGE_STYLES = {
     original: {
-      pixels: 1_638_400, # 1280x1280px
+      pixels: 2_073_600, # 1920x1080px
       file_geometry_parser: FastGeometryParser,
     }.freeze,
 
diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb
index 67e33875c..60be9b9dc 100644
--- a/app/services/fetch_oembed_service.rb
+++ b/app/services/fetch_oembed_service.rb
@@ -38,7 +38,17 @@ class FetchOEmbedService
 
     return if @endpoint_url.blank?
 
-    @endpoint_url = (Addressable::URI.parse(@url) + @endpoint_url).to_s
+    @endpoint_url = begin
+      base_url = Addressable::URI.parse(@url)
+
+      # If the OEmbed endpoint is given as http but the URL we opened
+      # was served over https, we can assume OEmbed will be available
+      # through https as well
+
+      (base_url + @endpoint_url).tap do |absolute_url|
+        absolute_url.scheme = base_url.scheme if base_url.scheme == 'https'
+      end.to_s
+    end
 
     cache_endpoint!
   rescue Addressable::URI::InvalidURIError
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
index b6e33ca91..08b4c852f 100644
--- a/app/views/layouts/modal.html.haml
+++ b/app/views/layouts/modal.html.haml
@@ -11,5 +11,6 @@
   .container-alt= yield
   .modal-layout__mastodon
     %div
+      %img{alt:'', draggable:'false', src:"#{mascot_url}"}
 
 = render template: 'layouts/application'