Browse Source

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 2 months ago
parent
commit
5e11f3a6e1
40 changed files with 1067 additions and 845 deletions
  1. +1
    -1
      .codeclimate.yml
  2. +4
    -4
      Gemfile
  3. +22
    -22
      Gemfile.lock
  4. +2
    -0
      Vagrantfile
  5. +4
    -5
      app/controllers/api/web/settings_controller.rb
  6. +1
    -1
      app/controllers/application_controller.rb
  7. +1
    -1
      app/controllers/instance_actors_controller.rb
  8. +13
    -0
      app/helpers/mascot_helper.rb
  9. +29
    -0
      app/javascript/mastodon/actions/boosts.js
  10. +2
    -2
      app/javascript/mastodon/actions/interactions.js
  11. +0
    -1
      app/javascript/mastodon/components/dropdown_menu.js
  12. +11
    -7
      app/javascript/mastodon/components/status_action_bar.js
  13. +0
    -1
      app/javascript/mastodon/containers/dropdown_menu_container.js
  14. +4
    -3
      app/javascript/mastodon/containers/status_container.js
  15. +12
    -6
      app/javascript/mastodon/features/compose/components/privacy_dropdown.js
  16. +0
    -1
      app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js
  17. +4
    -3
      app/javascript/mastodon/features/notifications/containers/notification_container.js
  18. +6
    -5
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  19. +4
    -3
      app/javascript/mastodon/features/status/components/action_bar.js
  20. +4
    -3
      app/javascript/mastodon/features/status/containers/detailed_status_container.js
  21. +4
    -3
      app/javascript/mastodon/features/status/index.js
  22. +35
    -3
      app/javascript/mastodon/features/ui/components/boost_modal.js
  23. +25
    -0
      app/javascript/mastodon/reducers/boosts.js
  24. +2
    -0
      app/javascript/mastodon/reducers/index.js
  25. +3
    -0
      app/javascript/mastodon/reducers/modal.js
  26. +1
    -1
      app/javascript/mastodon/utils/resize_image.js
  27. +10
    -0
      app/javascript/styles/mastodon/components.scss
  28. +11
    -2
      app/javascript/styles/mastodon/modal.scss
  29. +11
    -2
      app/lib/activitypub/activity/follow.rb
  30. +10
    -2
      app/lib/webfinger.rb
  31. +2
    -0
      app/models/concerns/account_finder_concern.rb
  32. +1
    -1
      app/models/media_attachment.rb
  33. +11
    -1
      app/services/fetch_oembed_service.rb
  34. +1
    -0
      app/views/layouts/modal.html.haml
  35. +2
    -0
      config/application.rb
  36. +15
    -0
      lib/action_dispatch/cookie_jar_extensions.rb
  37. +11
    -0
      lib/rails/engine_extensions.rb
  38. +15
    -15
      package.json
  39. +140
    -31
      spec/lib/activitypub/activity/follow_spec.rb
  40. +633
    -715
      yarn.lock

+ 1
- 1
.codeclimate.yml View File

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


+ 4
- 4
Gemfile View File

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


+ 22
- 22
Gemfile.lock View File

@@ -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
- 0
Vagrantfile View File

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


+ 4
- 5
app/controllers/api/web/settings_controller.rb View File

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

+ 1
- 1
app/controllers/application_controller.rb View File

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


+ 1
- 1
app/controllers/instance_actors_controller.rb View File

@@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
private

def set_account
@account = Account.find(-99)
@account = Account.representative
end

def restrict_fields_to


+ 13
- 0
app/helpers/mascot_helper.rb View File

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

+ 29
- 0
app/javascript/mastodon/actions/boosts.js View File

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

+ 2
- 2
app/javascript/mastodon/actions/interactions.js View File

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


+ 0
- 1
app/javascript/mastodon/components/dropdown_menu.js View File

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


+ 11
- 7
app/javascript/mastodon/components/status_action_bar.js View File

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


+ 0
- 1
app/javascript/mastodon/containers/dropdown_menu_container.js View File

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


+ 4
- 3
app/javascript/mastodon/containers/status_container.js View File

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



+ 12
- 6
app/javascript/mastodon/features/compose/components/privacy_dropdown.js View File

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


+ 0
- 1
app/javascript/mastodon/features/compose/containers/privacy_dropdown_container.js View File

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



+ 4
- 3
app/javascript/mastodon/features/notifications/containers/notification_container.js View File

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


+ 6
- 5
app/javascript/mastodon/features/picture_in_picture/components/footer.js View File

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



+ 4
- 3
app/javascript/mastodon/features/status/components/action_bar.js View File

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


+ 4
- 3
app/javascript/mastodon/features/status/containers/detailed_status_container.js View File

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


+ 4
- 3
app/javascript/mastodon/features/status/index.js View File

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


+ 35
- 3
app/javascript/mastodon/features/ui/components/boost_modal.js View File

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


+ 25
- 0
app/javascript/mastodon/reducers/boosts.js View File

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

+ 2
- 0
app/javascript/mastodon/reducers/index.js View File

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


+ 3
- 0
app/javascript/mastodon/reducers/modal.js View File

@@ -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
- 1
app/javascript/mastodon/utils/resize_image.js View File

@@ -1,6 +1,6 @@
import EXIF from 'exif-js';

const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px

const _browser_quirks = {};



+ 10
- 0
app/javascript/styles/mastodon/components.scss View File

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


+ 11
- 2
app/javascript/styles/mastodon/modal.scss View File

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



+ 11
- 2
app/lib/activitypub/activity/follow.rb View File

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


+ 10
- 2
app/lib/webfinger.rb View File

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

+ 2
- 0
app/models/concerns/account_finder_concern.rb View File

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


+ 1
- 1
app/models/media_attachment.rb View File

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



+ 11
- 1
app/services/fetch_oembed_service.rb View File

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


+ 1
- 0
app/views/layouts/modal.html.haml View File

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

= render template: 'layouts/application'

+ 2
- 0
config/application.rb View File

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



+ 15
- 0
lib/action_dispatch/cookie_jar_extensions.rb View File

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

+ 11
- 0
lib/rails/engine_extensions.rb View File

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

+ 15
- 15
package.json View File

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


+ 140
- 31
spec/lib/activitypub/activity/follow_spec.rb View File

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


+ 633
- 715
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save