diff options
Diffstat (limited to 'app')
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' |