Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `app/javascript/styles/mastodon/modal.scss`:
  For some reason we changed the file loading path in glitch-soc,
  but now upstream has completely changed how the logo is loaded.
  Applied upstream changes.
master
Claire 3 vuotta sitten
commit 5e11f3a6e1
  1. 2
      .codeclimate.yml
  2. 8
      Gemfile
  3. 44
      Gemfile.lock
  4. 2
      Vagrantfile
  5. 9
      app/controllers/api/web/settings_controller.rb
  6. 2
      app/controllers/application_controller.rb
  7. 2
      app/controllers/instance_actors_controller.rb
  8. 13
      app/helpers/mascot_helper.rb
  9. 29
      app/javascript/mastodon/actions/boosts.js
  10. 4
      app/javascript/mastodon/actions/interactions.js
  11. 1
      app/javascript/mastodon/components/dropdown_menu.js
  12. 18
      app/javascript/mastodon/components/status_action_bar.js
  13. 1
      app/javascript/mastodon/containers/dropdown_menu_container.js
  14. 7
      app/javascript/mastodon/containers/status_container.js
  15. 18
      app/javascript/mastodon/features/compose/components/privacy_dropdown.js
  16. 1
      app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
  17. 7
      app/javascript/mastodon/features/notifications/containers/notification_container.js
  18. 11
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  19. 7
      app/javascript/mastodon/features/status/components/action_bar.js
  20. 7
      app/javascript/mastodon/features/status/containers/detailed_status_container.js
  21. 7
      app/javascript/mastodon/features/status/index.js
  22. 38
      app/javascript/mastodon/features/ui/components/boost_modal.js
  23. 25
      app/javascript/mastodon/reducers/boosts.js
  24. 2
      app/javascript/mastodon/reducers/index.js
  25. 3
      app/javascript/mastodon/reducers/modal.js
  26. 2
      app/javascript/mastodon/utils/resize_image.js
  27. 10
      app/javascript/styles/mastodon/components.scss
  28. 13
      app/javascript/styles/mastodon/modal.scss
  29. 13
      app/lib/activitypub/activity/follow.rb
  30. 12
      app/lib/webfinger.rb
  31. 2
      app/models/concerns/account_finder_concern.rb
  32. 2
      app/models/media_attachment.rb
  33. 12
      app/services/fetch_oembed_service.rb
  34. 1
      app/views/layouts/modal.html.haml
  35. 2
      config/application.rb
  36. 15
      lib/action_dispatch/cookie_jar_extensions.rb
  37. 11
      lib/rails/engine_extensions.rb
  38. 30
      package.json
  39. 171
      spec/lib/activitypub/activity/follow_spec.rb
  40. 1348
      yarn.lock

@ -30,7 +30,7 @@ plugins:
channel: eslint-7
rubocop:
enabled: true
channel: rubocop-1-8-1
channel: rubocop-1-9-1
sass-lint:
enabled: true
exclude_patterns:

@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.7'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.87', require: false
gem 'aws-sdk-s3', '~> 1.88', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@ -27,7 +27,7 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.5', require: false
gem 'bootsnap', '~> 1.6.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
@ -73,7 +73,7 @@ gem 'parallel', '~> 1.20'
gem 'posix-spawn'
gem 'pundit', '~> 2.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.4'
gem 'rack-attack', '~> 6.5'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
@ -140,7 +140,7 @@ group :development do
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler'
gem 'rubocop', '~> 1.8', require: false
gem 'rubocop', '~> 1.9', require: false
gem 'rubocop-rails', '~> 2.9', require: false
gem 'brakeman', '~> 4.10', require: false
gem 'bundler-audit', '~> 0.7', require: false

@ -72,24 +72,24 @@ GEM
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
arel (9.0.0)
ast (2.4.1)
ast (2.4.2)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
awrence (1.1.1)
aws-eventstream (1.1.0)
aws-partitions (1.413.0)
aws-sdk-core (3.110.0)
aws-partitions (1.424.0)
aws-sdk-core (3.112.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.40.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (1.42.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.87.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-s3 (1.88.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
@ -104,7 +104,7 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.4)
ffi (~> 1.10.0)
bootsnap (1.5.1)
bootsnap (1.6.0)
msgpack (~> 1.0)
brakeman (4.10.1)
browser (4.2.0)
@ -275,7 +275,7 @@ GEM
httplog (1.4.3)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.8.7)
i18n (1.8.8)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.33)
activesupport (>= 4.0.2)
@ -293,7 +293,7 @@ GEM
jmespath (1.4.0)
json (2.3.1)
json-canonicalization (0.2.0)
json-ld (3.1.7)
json-ld (3.1.8)
htmlentities (~> 4.3)
json-canonicalization (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
@ -354,7 +354,7 @@ GEM
mini_mime (1.0.2)
mini_portile2 (2.5.0)
minitest (5.14.3)
msgpack (1.3.3)
msgpack (1.4.2)
multi_json (1.15.0)
multipart-post (2.1.1)
net-ldap (0.17.0)
@ -408,9 +408,9 @@ GEM
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.2.3)
pghero (2.7.3)
pghero (2.7.4)
activerecord (>= 5)
pkg-config (1.4.4)
pkg-config (1.4.5)
pluck_each (0.1.3)
activerecord (> 3.2.0)
activesupport (> 3.0.0)
@ -439,7 +439,7 @@ GEM
raabro (1.3.3)
racc (1.5.2)
rack (2.2.3)
rack-attack (6.4.0)
rack-attack (6.5.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
@ -482,7 +482,7 @@ GEM
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (13.0.3)
rdf (3.1.8)
rdf (3.1.10)
hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.4.0)
@ -496,7 +496,7 @@ GEM
redis-activesupport (5.2.0)
activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2)
redis-namespace (1.8.0)
redis-namespace (1.8.1)
redis (>= 3.0.4)
redis-rack (2.1.3)
rack (>= 2.0.8, < 3)
@ -542,7 +542,7 @@ GEM
rspec-support (3.10.1)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.8.1)
rubocop (1.9.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -551,7 +551,7 @@ GEM
rubocop-ast (>= 1.2.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.4.0)
rubocop-ast (1.4.1)
parser (>= 2.7.1.5)
rubocop-rails (2.9.1)
activesupport (>= 4.2.0)
@ -693,11 +693,11 @@ DEPENDENCIES
active_record_query_trace (~> 1.8)
addressable (~> 2.7)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.87)
aws-sdk-s3 (~> 1.88)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.5)
bootsnap (~> 1.6.0)
brakeman (~> 4.10)
browser
bullet (~> 6.1)
@ -777,7 +777,7 @@ DEPENDENCIES
puma (~> 5.1)
pundit (~> 2.1)
rack (~> 2.2.3)
rack-attack (~> 6.4)
rack-attack (~> 6.5)
rack-cors (~> 1.1)
rails (~> 5.2.4.4)
rails-controller-testing (~> 1.0)
@ -792,7 +792,7 @@ DEPENDENCIES
rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4)
rubocop (~> 1.8)
rubocop (~> 1.9)
rubocop-rails (~> 2.9)
ruby-progressbar (~> 1.11)
sanitize (~> 5.2)

2
Vagrantfile vendored

@ -72,10 +72,12 @@ bundle install
yarn install
# Build Mastodon
export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup
# Configure automatic loading of environment variable
echo 'export RAILS_ENV=development' >> ~/.bash_profile
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
SCRIPT

@ -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

@ -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?

@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
private
def set_account
@account = Account.find(-99)
@account = Account.representative
end
def restrict_fields_to

@ -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

@ -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,
});
};
}

@ -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));

@ -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,

@ -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 {

@ -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']),

@ -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 }));
}
},

@ -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}

@ -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']),
});

@ -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 }));
}
}
},

@ -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 }));
}
};

@ -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 });

@ -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 }));
}
}
},

@ -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 }));
}
}
}

@ -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>

@ -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;
}
}

@ -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,

@ -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;
}

@ -1,6 +1,6 @@
import EXIF from 'exif-js';
const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
const _browser_quirks = {};

@ -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;

@ -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;
}
}
}

@ -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

@ -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

@ -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)

@ -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,

@ -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

@ -11,5 +11,6 @@
.container-alt= yield
.modal-layout__mastodon
%div
%img{alt:'', draggable:'false', src:"#{mascot_url}"}
= render template: 'layouts/application'

@ -25,6 +25,8 @@ require_relative '../lib/devise/two_factor_pam_authenticatable'
require_relative '../lib/chewy/strategy/custom_sidekiq'
require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/action_dispatch/cookie_jar_extensions'
require_relative '../lib/rails/engine_extensions'
Dotenv::Railtie.load

@ -0,0 +1,15 @@
# frozen_string_literal: true
module ActionDispatch
module CookieJarExtensions
private
# Monkey-patch ActionDispatch to serve secure cookies to Tor Hidden Service
# users. Otherwise, ActionDispatch would drop the cookie over HTTP.
def write_cookie?(*)
request.headers['Host'].ends_with?('.onion') || super
end
end
end
ActionDispatch::Cookies::CookieJar.prepend(ActionDispatch::CookieJarExtensions)

@ -0,0 +1,11 @@
module Rails
module EngineExtensions
# Rewrite task loading code to filter digitalocean.rake task
def run_tasks_blocks(app)
Railtie.instance_method(:run_tasks_blocks).bind(self).call(app)
paths["lib/tasks"].existent.reject { |ext| ext.end_with?('digitalocean.rake') }.sort.each { |ext| load(ext) }
end
end
end
Rails::Engine.prepend(Rails::EngineExtensions)

@ -60,18 +60,18 @@
},
"private": true,
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/core": "^7.12.16",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.12.12",
"@babel/plugin-transform-react-inline-elements": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/runtime": "^7.12.5",
"@babel/plugin-proposal-decorators": "^7.12.13",
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.12.15",
"@babel/preset-env": "^7.12.16",
"@babel/preset-react": "^7.12.13",
"@babel/runtime": "^7.12.13",
"@clusterws/cws": "^3.0.0",
"@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.5.7",
"@rails/ujs": "^6.1.1",
"@rails/ujs": "^6.1.2",
"array-includes": "^3.1.2",
"atrament": "0.2.4",
"arrow-key-navigation": "^1.2.0",
@ -88,7 +88,7 @@
"color-blend": "^3.0.1",
"compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3",
"css-loader": "^5.0.1",
"css-loader": "^5.0.2",
"cssnano": "^4.1.10",
"detect-passive-events": "^2.0.2",
"dotenv": "^8.2.0",
@ -114,7 +114,7 @@
"lodash": "^4.17.19",
"mark-loader": "^0.1.6",
"marky": "^1.2.1",
"mini-css-extract-plugin": "^1.3.5",
"mini-css-extract-plugin": "^1.3.6",
"mkdirp": "^1.0.4",
"npmlog": "^4.1.2",
"object-assign": "^4.1.1",
@ -145,7 +145,7 @@
"react-select": "^3.2.0",
"react-sparklines": "^1.7.0",
"react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.3.0",
"react-textarea-autosize": "^8.3.1",
"react-toggle": "^4.1.1",
"redis": "^3.0.2",
"redux": "^4.0.5",
@ -156,7 +156,7 @@
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
"rimraf": "^3.0.2",
"sass": "^1.32.5",
"sass": "^1.32.7",
"sass-loader": "^10.1.1",
"stacktrace-js": "^2.0.2",
"stringz": "^2.1.0",
@ -175,13 +175,13 @@
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.3",
"@testing-library/react": "^11.2.5",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"eslint": "^7.18.0",
"eslint": "^7.19.0",
"eslint-plugin-import": "~2.22.1",
"eslint-plugin-jsx-a11y": "~6.4.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-promise": "~4.3.1",
"eslint-plugin-react": "~7.22.0",
"jest": "^26.6.3",
"raf": "^3.4.1",

@ -17,62 +17,171 @@ RSpec.describe ActivityPub::Activity::Follow do
describe '#perform' do
subject { described_class.new(json, sender) }
context 'unlocked account' do
before do
subject.perform
context 'with no prior follow' do
context 'unlocked account' do
before do
subject.perform
end
it 'creates a follow from sender to recipient' do
expect(sender.following?(recipient)).to be true
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
it 'creates a follow from sender to recipient' do
expect(sender.following?(recipient)).to be true
context 'silenced account following an unlocked account' do
before do
sender.touch(:silenced_at)
subject.perform
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
end
it 'creates a follow request' do
expect(sender.requested?(recipient)).to be true
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
end
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
context 'unlocked account muting the sender' do
before do
recipient.mute!(sender)
subject.perform
end
it 'creates a follow from sender to recipient' do
expect(sender.following?(recipient)).to be true
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
context 'locked account' do
before do
recipient.update(locked: true)
subject.perform
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
end
it 'creates a follow request' do
expect(sender.requested?(recipient)).to be true
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
end
end
end
context 'silenced account following an unlocked account' do
context 'when a follow relationship already exists' do
before do
sender.touch(:silenced_at)
subject.perform
sender.active_relationships.create!(target_account: recipient, uri: 'bar')
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
end
context 'unlocked account' do
before do
subject.perform
end
it 'correctly sets the new URI' do
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'creates a follow request' do
expect(sender.requested?(recipient)).to be true
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
end
context 'unlocked account muting the sender' do
before do
recipient.mute!(sender)
subject.perform
context 'silenced account following an unlocked account' do
before do
sender.touch(:silenced_at)
subject.perform
end
it 'correctly sets the new URI' do
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
it 'creates a follow from sender to recipient' do
expect(sender.following?(recipient)).to be true
context 'unlocked account muting the sender' do
before do
recipient.mute!(sender)
subject.perform
end
it 'correctly sets the new URI' do
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
context 'locked account' do
before do
recipient.update(locked: true)
subject.perform
end
it 'correctly sets the new URI' do
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
end
it 'does not create a follow request' do
expect(sender.requested?(recipient)).to be false
end
end
end
context 'locked account' do
context 'when a follow request already exists' do
before do
recipient.update(locked: true)
subject.perform
sender.follow_requests.create!(target_account: recipient, uri: 'bar')
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
context 'silenced account following an unlocked account' do
before do
sender.touch(:silenced_at)
subject.perform
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
end
it 'correctly sets the new URI' do
expect(sender.requested?(recipient)).to be true
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
end
end
it 'creates a follow request' do
expect(sender.requested?(recipient)).to be true
context 'locked account' do
before do
recipient.update(locked: true)
subject.perform
end
it 'does not create a follow from sender to recipient' do
expect(sender.following?(recipient)).to be false
end
it 'correctly sets the new URI' do
expect(sender.requested?(recipient)).to be true
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
end
end
end
end

File diff suppressed because it is too large Load Diff
Ladataan…
Peruuta
Tallenna