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.
th-downstream
Claire 4 years ago
commit 489a5a539c

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

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

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

2
Vagrantfile vendored

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

@ -2,17 +2,16 @@
class Api::Web::SettingsController < Api::Web::BaseController class Api::Web::SettingsController < Api::Web::BaseController
before_action :require_user! before_action :require_user!
before_action :set_setting
def update def update
setting.data = params[:data] @setting.update!(data: params[:data])
setting.save!
render_empty render_empty
end end
private private
def setting def set_setting
@_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user) @setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
end end
end end

@ -44,7 +44,7 @@ class ApplicationController < ActionController::Base
private private
def https_enabled? 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 end
def authorized_fetch_mode? def authorized_fetch_mode?

@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
private private
def set_account def set_account
@account = Account.find(-99) @account = Account.representative
end end
def restrict_fields_to 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_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
export function reblog(status) { export function reblog(status, visibility) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(reblogRequest(status)); 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 // 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 // interested in how the original is modified, hence passing it skipping the wrapper
dispatch(importFetchedStatus(response.data.reblog)); dispatch(importFetchedStatus(response.data.reblog));

@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent {
disabled: PropTypes.bool, disabled: PropTypes.bool,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired, onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string, dropdownPlacement: PropTypes.string,

@ -223,10 +223,11 @@ class StatusActionBar extends ImmutablePureComponent {
render () { render () {
const { status, relationship, intl, withDismiss, scrollKey } = this.props; const { status, relationship, intl, withDismiss, scrollKey } = this.props;
const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;
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 account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
let menu = []; let menu = [];
@ -237,19 +238,22 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); 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 }); 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); 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({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null); menu.push(null);
} }
if (status.getIn(['account', 'id']) === me) { if (writtenByMe) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else { } else {

@ -6,7 +6,6 @@ import DropdownMenu from '../components/dropdown_menu';
import { isUserTouching } from '../is_mobile'; import { isUserTouching } from '../is_mobile';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']), openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),

@ -35,6 +35,7 @@ import {
} from '../actions/domain_blocks'; } from '../actions/domain_blocks';
import { initMuteModal } from '../actions/mutes'; import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks'; import { initBlockModal } from '../actions/blocks';
import { initBoostModal } from '../actions/boosts';
import { initReport } from '../actions/reports'; import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture'; import { deployPictureInPicture } from '../actions/picture_in_picture';
@ -82,11 +83,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}); });
}, },
onModalReblog (status) { onModalReblog (status, privacy) {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else { } else {
dispatch(reblog(status)); dispatch(reblog(status, privacy));
} }
}, },
@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if ((e && e.shiftKey) || !boostModal) { if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } 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 // It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by // size will be used to determine the coordinate of the menu by
// react-overlays // 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 => ( {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 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'> <div className='privacy-dropdown__option__icon'>
@ -153,11 +153,12 @@ class PrivacyDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onModalOpen: PropTypes.func, onModalOpen: PropTypes.func,
onModalClose: PropTypes.func, onModalClose: PropTypes.func,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
container: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -167,7 +168,7 @@ class PrivacyDropdown extends React.PureComponent {
}; };
handleToggle = ({ target }) => { handleToggle = ({ target }) => {
if (this.props.isUserTouching()) { if (this.props.isUserTouching && this.props.isUserTouching()) {
if (this.state.open) { if (this.state.open) {
this.props.onModalClose(); this.props.onModalClose();
} else { } 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: '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: '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: '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 () { render () {
const { value, intl } = this.props; const { value, container, intl } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
const valueOption = this.options.find(item => item.value === value); const valueOption = this.options.find(item => item.value === value);
@ -264,7 +270,7 @@ class PrivacyDropdown extends React.PureComponent {
/> />
</div> </div>
<Overlay show={open} placement={placement} target={this}> <Overlay show={open} placement={placement} target={this} container={container}>
<PrivacyDropdownMenu <PrivacyDropdownMenu
items={this.options} items={this.options}
value={value} value={value}

@ -5,7 +5,6 @@ import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile'; import { isUserTouching } from '../../../is_mobile';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
value: state.getIn(['compose', 'privacy']), value: state.getIn(['compose', 'privacy']),
}); });

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeGetNotification, makeGetStatus } from '../../../selectors'; import { makeGetNotification, makeGetStatus } from '../../../selectors';
import Notification from '../components/notification'; import Notification from '../components/notification';
import { initBoostModal } from '../../../actions/boosts';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { mentionCompose } from '../../../actions/compose'; import { mentionCompose } from '../../../actions/compose';
import { import {
@ -35,8 +36,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onModalReblog (status) { onModalReblog (status, privacy) {
dispatch(reblog(status)); dispatch(reblog(status, privacy));
}, },
onReblog (status, e) { onReblog (status, e) {
@ -46,7 +47,7 @@ const mapDispatchToProps = dispatch => ({
if (e.shiftKey || !boostModal) { if (e.shiftKey || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } 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 { replyCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
import { initBoostModal } from 'mastodon/actions/boosts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
const messages = defineMessages({ const messages = defineMessages({
@ -89,9 +90,9 @@ class Footer extends ImmutablePureComponent {
} }
}; };
_performReblog = () => { _performReblog = (status, privacy) => {
const { dispatch, status } = this.props; const { dispatch } = this.props;
dispatch(reblog(status)); dispatch(reblog(status, privacy));
} }
handleReblogClick = e => { handleReblogClick = e => {
@ -100,9 +101,9 @@ class Footer extends ImmutablePureComponent {
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else if ((e && e.shiftKey) || !boostModal) { } else if ((e && e.shiftKey) || !boostModal) {
this._performReblog(); this._performReblog(status);
} else { } else {
dispatch(openModal('BOOST', { status, onReblog: this._performReblog })); dispatch(initBoostModal({ status, onReblog: this._performReblog }));
} }
}; };

@ -187,9 +187,10 @@ class ActionBar extends React.PureComponent {
render () { render () {
const { status, relationship, intl } = this.props; 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 mutingConversation = status.get('muted');
const account = status.get('account'); const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
let menu = []; let menu = [];
@ -199,12 +200,12 @@ class ActionBar extends React.PureComponent {
menu.push(null); menu.push(null);
} }
if (me === status.getIn(['account', 'id'])) { if (writtenByMe) {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); 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({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });

@ -23,6 +23,7 @@ import {
} from '../../../actions/statuses'; } from '../../../actions/statuses';
import { initMuteModal } from '../../../actions/mutes'; import { initMuteModal } from '../../../actions/mutes';
import { initBlockModal } from '../../../actions/blocks'; import { initBlockModal } from '../../../actions/blocks';
import { initBoostModal } from '../../../actions/boosts';
import { initReport } from '../../../actions/reports'; import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
@ -68,8 +69,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}); });
}, },
onModalReblog (status) { onModalReblog (status, privacy) {
dispatch(reblog(status)); dispatch(reblog(status, privacy));
}, },
onReblog (status, e) { onReblog (status, e) {
@ -79,7 +80,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (e.shiftKey || !boostModal) { if (e.shiftKey || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
} }
} }
}, },

@ -42,6 +42,7 @@ import {
} from '../../actions/domain_blocks'; } from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes'; import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll-4';
@ -234,8 +235,8 @@ class Status extends ImmutablePureComponent {
} }
} }
handleModalReblog = (status) => { handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(status)); this.props.dispatch(reblog(status, privacy));
} }
handleReblogClick = (status, e) => { handleReblogClick = (status, e) => {
@ -245,7 +246,7 @@ class Status extends ImmutablePureComponent {
if ((e && e.shiftKey) || !boostModal) { if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status); this.handleModalReblog(status);
} else { } 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 React from 'react';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; 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 ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import AttachmentList from 'mastodon/components/attachment_list'; import AttachmentList from 'mastodon/components/attachment_list';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import classNames from 'classnames'; import classNames from 'classnames';
import { changeBoostPrivacy } from 'mastodon/actions/boosts';
const messages = defineMessages({ const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@ -21,7 +24,22 @@ const messages = defineMessages({
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, 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 { class BoostModal extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
@ -32,6 +50,8 @@ class BoostModal extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChangeBoostPrivacy: PropTypes.func.isRequired,
privacy: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -40,7 +60,7 @@ class BoostModal extends ImmutablePureComponent {
} }
handleReblog = () => { handleReblog = () => {
this.props.onReblog(this.props.status); this.props.onReblog(this.props.status, this.props.privacy);
this.props.onClose(); this.props.onClose();
} }
@ -52,12 +72,16 @@ class BoostModal extends ImmutablePureComponent {
} }
} }
_findContainer = () => {
return document.getElementsByClassName('modal-root__container')[0];
};
setRef = (c) => { setRef = (c) => {
this.button = c; this.button = c;
} }
render () { render () {
const { status, intl } = this.props; const { status, privacy, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
const visibilityIconInfo = { const visibilityIconInfo = {
@ -102,6 +126,14 @@ class BoostModal extends ImmutablePureComponent {
<div className='boost-modal__action-bar'> <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> <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} /> <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
</div> </div>
</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 status_lists from './status_lists';
import mutes from './mutes'; import mutes from './mutes';
import blocks from './blocks'; import blocks from './blocks';
import boosts from './boosts';
import reports from './reports'; import reports from './reports';
import contexts from './contexts'; import contexts from './contexts';
import compose from './compose'; import compose from './compose';
@ -57,6 +58,7 @@ const reducers = {
push_notifications, push_notifications,
mutes, mutes,
blocks, blocks,
boosts,
reports, reports,
contexts, contexts,
compose, compose,

@ -1,4 +1,5 @@
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
import { TIMELINE_DELETE } from '../actions/timelines';
const initialState = { const initialState = {
modalType: null, modalType: null,
@ -11,6 +12,8 @@ export default function modal(state = initialState, action) {
return { modalType: action.modalType, modalProps: action.modalProps }; return { modalType: action.modalType, modalProps: action.modalProps };
case MODAL_CLOSE: case MODAL_CLOSE:
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state; return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
case TIMELINE_DELETE:
return (state.modalProps.statusId === action.id) ? initialState : state;
default: default:
return state; return state;
} }

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

@ -4209,6 +4209,7 @@ a.status-card.compact:hover {
border-radius: 4px; border-radius: 4px;
margin-left: 40px; margin-left: 40px;
overflow: hidden; overflow: hidden;
z-index: 2;
&.top { &.top {
transform-origin: 50% 100%; 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 { .privacy-dropdown__option {
color: $inverted-text-color; color: $inverted-text-color;
padding: 10px; padding: 10px;

@ -12,10 +12,19 @@
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
> * { > div {
flex: 1; flex: 1;
max-height: 235px; 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 def perform
target_account = account_from_uri(object_uri) 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? if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
reject_follow_request!(target_account) reject_follow_request!(target_account)
@ -14,7 +21,9 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
end end
# Fast-forward repeat follow requests # 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']) AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
return return
end end

@ -88,10 +88,18 @@ class Webfinger
end end
def standard_url 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 end
def host_meta_url 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
end end

@ -14,6 +14,8 @@ module AccountFinderConcern
def representative def representative
Account.find(-99) Account.find(-99)
rescue ActiveRecord::RecordNotFound
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
end end
def find_local(username) def find_local(username)

@ -59,7 +59,7 @@ class MediaAttachment < ApplicationRecord
IMAGE_STYLES = { IMAGE_STYLES = {
original: { original: {
pixels: 1_638_400, # 1280x1280px pixels: 2_073_600, # 1920x1080px
file_geometry_parser: FastGeometryParser, file_geometry_parser: FastGeometryParser,
}.freeze, }.freeze,

@ -38,7 +38,17 @@ class FetchOEmbedService
return if @endpoint_url.blank? 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! cache_endpoint!
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError

@ -11,5 +11,6 @@
.container-alt= yield .container-alt= yield
.modal-layout__mastodon .modal-layout__mastodon
%div %div
%img{alt:'', draggable:'false', src:"#{mascot_url}"}
= render template: 'layouts/application' = 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/chewy/strategy/custom_sidekiq'
require_relative '../lib/webpacker/manifest_extensions' require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions' require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/action_dispatch/cookie_jar_extensions'
require_relative '../lib/rails/engine_extensions'
Dotenv::Railtie.load 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, "private": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.12.16",
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.12.12", "@babel/plugin-proposal-decorators": "^7.12.13",
"@babel/plugin-transform-react-inline-elements": "^7.12.1", "@babel/plugin-transform-react-inline-elements": "^7.12.13",
"@babel/plugin-transform-runtime": "^7.12.10", "@babel/plugin-transform-runtime": "^7.12.15",
"@babel/preset-env": "^7.12.11", "@babel/preset-env": "^7.12.16",
"@babel/preset-react": "^7.12.10", "@babel/preset-react": "^7.12.13",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.13",
"@clusterws/cws": "^3.0.0", "@clusterws/cws": "^3.0.0",
"@gamestdio/websocket": "^0.3.2", "@gamestdio/websocket": "^0.3.2",
"@github/webauthn-json": "^0.5.7", "@github/webauthn-json": "^0.5.7",
"@rails/ujs": "^6.1.1", "@rails/ujs": "^6.1.2",
"array-includes": "^3.1.2", "array-includes": "^3.1.2",
"atrament": "0.2.4", "atrament": "0.2.4",
"arrow-key-navigation": "^1.2.0", "arrow-key-navigation": "^1.2.0",
@ -88,7 +88,7 @@
"color-blend": "^3.0.1", "color-blend": "^3.0.1",
"compression-webpack-plugin": "^6.1.1", "compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^5.0.1", "css-loader": "^5.0.2",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"detect-passive-events": "^2.0.2", "detect-passive-events": "^2.0.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
@ -114,7 +114,7 @@
"lodash": "^4.17.19", "lodash": "^4.17.19",
"mark-loader": "^0.1.6", "mark-loader": "^0.1.6",
"marky": "^1.2.1", "marky": "^1.2.1",
"mini-css-extract-plugin": "^1.3.5", "mini-css-extract-plugin": "^1.3.6",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"object-assign": "^4.1.1", "object-assign": "^4.1.1",
@ -145,7 +145,7 @@
"react-select": "^3.2.0", "react-select": "^3.2.0",
"react-sparklines": "^1.7.0", "react-sparklines": "^1.7.0",
"react-swipeable-views": "^0.13.9", "react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.3.0", "react-textarea-autosize": "^8.3.1",
"react-toggle": "^4.1.1", "react-toggle": "^4.1.1",
"redis": "^3.0.2", "redis": "^3.0.2",
"redux": "^4.0.5", "redux": "^4.0.5",
@ -156,7 +156,7 @@
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "^1.32.5", "sass": "^1.32.7",
"sass-loader": "^10.1.1", "sass-loader": "^10.1.1",
"stacktrace-js": "^2.0.2", "stacktrace-js": "^2.0.2",
"stringz": "^2.1.0", "stringz": "^2.1.0",
@ -175,13 +175,13 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.11.9", "@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-eslint": "^10.1.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"eslint": "^7.18.0", "eslint": "^7.19.0",
"eslint-plugin-import": "~2.22.1", "eslint-plugin-import": "~2.22.1",
"eslint-plugin-jsx-a11y": "~6.4.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", "eslint-plugin-react": "~7.22.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"raf": "^3.4.1", "raf": "^3.4.1",

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save