Merge pull request #1500 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
8792128f38
@ -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:
|
||||
|
8
Gemfile
8
Gemfile
@ -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
|
||||
|
44
Gemfile.lock
44
Gemfile.lock
@ -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
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
|
||||
|
13
app/helpers/mascot_helper.rb
Normal file
13
app/helpers/mascot_helper.rb
Normal 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
app/javascript/flavours/glitch/actions/boosts.js
Normal file
29
app/javascript/flavours/glitch/actions/boosts.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
@ -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,
|
||||
|
@ -193,9 +193,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props;
|
||||
|
||||
const mutingConversation = status.get('muted');
|
||||
const anonymousAccess = !me;
|
||||
const mutingConversation = status.get('muted');
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||
|
||||
let menu = [];
|
||||
let reblogIcon = 'retweet';
|
||||
@ -211,16 +212,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (status.getIn(['account', 'id']) === me || withDismiss) {
|
||||
if (writtenByMe && publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -5,7 +5,6 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||
import { isUserTouching } from 'flavours/glitch/util/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']),
|
||||
|
@ -21,6 +21,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||
@ -96,11 +97,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
});
|
||||
},
|
||||
|
||||
onModalReblog (status) {
|
||||
onModalReblog (status, privacy) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
dispatch(reblog(status));
|
||||
dispatch(reblog(status, privacy));
|
||||
}
|
||||
},
|
||||
|
||||
@ -108,11 +109,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog, missingMediaDescription: true }));
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog, missingMediaDescription: true }));
|
||||
} else if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -31,6 +31,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
noModal: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -42,10 +44,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||
|
||||
// Toggles opening and closing the dropdown.
|
||||
handleToggle = ({ target, type }) => {
|
||||
const { onModalOpen } = this.props;
|
||||
const { onModalOpen, noModal } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
if (isUserTouching()) {
|
||||
if (!noModal && isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
@ -183,6 +185,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||
items,
|
||||
onChange,
|
||||
value,
|
||||
container,
|
||||
} = this.props;
|
||||
const { open, placement } = this.state;
|
||||
const computedClass = classNames('composer--options--dropdown', {
|
||||
@ -219,6 +222,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||
placement={placement}
|
||||
show={open}
|
||||
target={this}
|
||||
container={container}
|
||||
>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
|
@ -9,6 +9,7 @@ import spring from 'react-motion/lib/spring';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import TextIconButton from './text_icon_button';
|
||||
import Dropdown from './dropdown';
|
||||
import PrivacyDropdown from './privacy_dropdown';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
// Utils.
|
||||
@ -25,22 +26,10 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Attach...',
|
||||
id: 'compose.attach',
|
||||
},
|
||||
change_privacy: {
|
||||
defaultMessage: 'Adjust status privacy',
|
||||
id: 'privacy.change',
|
||||
},
|
||||
content_type: {
|
||||
defaultMessage: 'Content type',
|
||||
id: 'content-type.change',
|
||||
},
|
||||
direct_long: {
|
||||
defaultMessage: 'Visible for mentioned users only',
|
||||
id: 'privacy.direct.long',
|
||||
},
|
||||
direct_short: {
|
||||
defaultMessage: 'Direct',
|
||||
id: 'privacy.direct.short',
|
||||
},
|
||||
doodle: {
|
||||
defaultMessage: 'Draw something',
|
||||
id: 'compose.attach.doodle',
|
||||
@ -65,22 +54,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Plain text',
|
||||
id: 'compose.content-type.plain',
|
||||
},
|
||||
private_long: {
|
||||
defaultMessage: 'Visible for followers only',
|
||||
id: 'privacy.private.long',
|
||||
},
|
||||
private_short: {
|
||||
defaultMessage: 'Followers-only',
|
||||
id: 'privacy.private.short',
|
||||
},
|
||||
public_long: {
|
||||
defaultMessage: 'Visible for all, shown in public timelines',
|
||||
id: 'privacy.public.long',
|
||||
},
|
||||
public_short: {
|
||||
defaultMessage: 'Public',
|
||||
id: 'privacy.public.short',
|
||||
},
|
||||
spoiler: {
|
||||
defaultMessage: 'Hide text behind warning',
|
||||
id: 'compose_form.spoiler',
|
||||
@ -93,14 +66,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Threaded mode',
|
||||
id: 'advanced_options.threaded_mode.short',
|
||||
},
|
||||
unlisted_long: {
|
||||
defaultMessage: 'Visible for all, but not in public timelines',
|
||||
id: 'privacy.unlisted.long',
|
||||
},
|
||||
unlisted_short: {
|
||||
defaultMessage: 'Unlisted',
|
||||
id: 'privacy.unlisted.short',
|
||||
},
|
||||
upload: {
|
||||
defaultMessage: 'Upload a file',
|
||||
id: 'compose.attach.upload',
|
||||
@ -201,35 +166,6 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||
showContentTypeChoice,
|
||||
} = this.props;
|
||||
|
||||
// We predefine our privacy items so that we can easily pick the
|
||||
// dropdown icon later.
|
||||
const privacyItems = {
|
||||
direct: {
|
||||
icon: 'envelope',
|
||||
meta: <FormattedMessage {...messages.direct_long} />,
|
||||
name: 'direct',
|
||||
text: <FormattedMessage {...messages.direct_short} />,
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
meta: <FormattedMessage {...messages.private_long} />,
|
||||
name: 'private',
|
||||
text: <FormattedMessage {...messages.private_short} />,
|
||||
},
|
||||
public: {
|
||||
icon: 'globe',
|
||||
meta: <FormattedMessage {...messages.public_long} />,
|
||||
name: 'public',
|
||||
text: <FormattedMessage {...messages.public_short} />,
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
meta: <FormattedMessage {...messages.unlisted_long} />,
|
||||
name: 'unlisted',
|
||||
text: <FormattedMessage {...messages.unlisted_short} />,
|
||||
},
|
||||
};
|
||||
|
||||
const contentTypeItems = {
|
||||
plain: {
|
||||
icon: 'file-text',
|
||||
@ -297,19 +233,11 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||
/>
|
||||
)}
|
||||
<hr />
|
||||
<Dropdown
|
||||
<PrivacyDropdown
|
||||
disabled={disabled}
|
||||
icon={(privacyItems[privacy] || {}).icon}
|
||||
items={[
|
||||
privacyItems.public,
|
||||
privacyItems.unlisted,
|
||||
privacyItems.private,
|
||||
privacyItems.direct,
|
||||
]}
|
||||
onChange={onChangeVisibility}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
value={privacy}
|
||||
/>
|
||||
{showContentTypeChoice && (
|
||||
|
@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import Dropdown from './dropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
change_privacy: {
|
||||
defaultMessage: 'Adjust status privacy',
|
||||
id: 'privacy.change',
|
||||
},
|
||||
direct_long: {
|
||||
defaultMessage: 'Visible for mentioned users only',
|
||||
id: 'privacy.direct.long',
|
||||
},
|
||||
direct_short: {
|
||||
defaultMessage: 'Direct',
|
||||
id: 'privacy.direct.short',
|
||||
},
|
||||
private_long: {
|
||||
defaultMessage: 'Visible for followers only',
|
||||
id: 'privacy.private.long',
|
||||
},
|
||||
private_short: {
|
||||
defaultMessage: 'Followers-only',
|
||||
id: 'privacy.private.short',
|
||||
},
|
||||
public_long: {
|
||||
defaultMessage: 'Visible for all, shown in public timelines',
|
||||
id: 'privacy.public.long',
|
||||
},
|
||||
public_short: {
|
||||
defaultMessage: 'Public',
|
||||
id: 'privacy.public.short',
|
||||
},
|
||||
unlisted_long: {
|
||||
defaultMessage: 'Visible for all, but not in public timelines',
|
||||
id: 'privacy.unlisted.long',
|
||||
},
|
||||
unlisted_short: {
|
||||
defaultMessage: 'Unlisted',
|
||||
id: 'privacy.unlisted.short',
|
||||
},
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class PrivacyDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
noDirect: PropTypes.bool,
|
||||
noModal: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props;
|
||||
|
||||
// We predefine our privacy items so that we can easily pick the
|
||||
// dropdown icon later.
|
||||
const privacyItems = {
|
||||
direct: {
|
||||
icon: 'envelope',
|
||||
meta: <FormattedMessage {...messages.direct_long} />,
|
||||
name: 'direct',
|
||||
text: <FormattedMessage {...messages.direct_short} />,
|
||||
},
|
||||
private: {
|
||||
icon: 'lock',
|
||||
meta: <FormattedMessage {...messages.private_long} />,
|
||||
name: 'private',
|
||||
text: <FormattedMessage {...messages.private_short} />,
|
||||
},
|
||||
public: {
|
||||
icon: 'globe',
|
||||
meta: <FormattedMessage {...messages.public_long} />,
|
||||
name: 'public',
|
||||
text: <FormattedMessage {...messages.public_short} />,
|
||||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
meta: <FormattedMessage {...messages.unlisted_long} />,
|
||||
name: 'unlisted',
|
||||
text: <FormattedMessage {...messages.unlisted_short} />,
|
||||
},
|
||||
};
|
||||
|
||||
const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private];
|
||||
|
||||
if (!noDirect) {
|
||||
items.push(privacyItems.direct);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
icon={(privacyItems[value] || {}).icon}
|
||||
items={items}
|
||||
onChange={onChange}
|
||||
onModalClose={onModalClose}
|
||||
onModalOpen={onModalOpen}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
container={container}
|
||||
noModal={noModal}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -11,6 +11,7 @@ import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
|
||||
const messages = defineMessages({
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
@ -82,9 +83,9 @@ class Footer extends ImmutablePureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_performReblog = () => {
|
||||
_performReblog = (privacy) => {
|
||||
const { dispatch, status } = this.props;
|
||||
dispatch(reblog(status));
|
||||
dispatch(reblog(status, privacy));
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
@ -95,7 +96,7 @@ class Footer extends ImmutablePureComponent {
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this._performReblog();
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
|
||||
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -145,8 +145,9 @@ class ActionBar extends React.PureComponent {
|
||||
render () {
|
||||
const { status, 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 writtenByMe = status.getIn(['account', 'id']) === me;
|
||||
|
||||
let menu = [];
|
||||
|
||||
@ -156,12 +157,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 });
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||
@ -67,8 +68,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
});
|
||||
},
|
||||
|
||||
onModalReblog (status) {
|
||||
dispatch(reblog(status));
|
||||
onModalReblog (status, privacy) {
|
||||
dispatch(reblog(status, privacy));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
@ -78,7 +79,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 }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -30,6 +30,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initReport } from 'flavours/glitch/actions/reports';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||
import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
||||
@ -262,13 +263,13 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleModalReblog = (status) => {
|
||||
handleModalReblog = (status, privacy) => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else {
|
||||
dispatch(reblog(status));
|
||||
dispatch(reblog(status, privacy));
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,11 +277,11 @@ class Status extends ImmutablePureComponent {
|
||||
const { settings, dispatch } = this.props;
|
||||
|
||||
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this.handleModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
||||
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 'flavours/glitch/components/display_name';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
||||
import classNames from 'classnames';
|
||||
import { changeBoostPrivacy } from 'flavours/glitch/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 = {
|
||||
@ -41,7 +59,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
handleReblog = () => {
|
||||
this.props.onReblog(this.props.status);
|
||||
this.props.onReblog(this.props.status, this.props.privacy);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
@ -55,12 +73,16 @@ class BoostModal extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_findContainer = () => {
|
||||
return document.getElementsByClassName('modal-root__container')[0];
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.button = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, missingMediaDescription, intl } = this.props;
|
||||
const { status, missingMediaDescription, privacy, intl } = this.props;
|
||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||
|
||||
const visibilityIconInfo = {
|
||||
@ -111,6 +133,15 @@ class BoostModal extends ImmutablePureComponent {
|
||||
<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
app/javascript/flavours/glitch/reducers/boosts.js
Normal file
25
app/javascript/flavours/glitch/reducers/boosts.js
Normal file
@ -0,0 +1,25 @@
|
||||
import Immutable from 'immutable';
|
||||
|
||||
import {
|
||||
BOOSTS_INIT_MODAL,
|
||||
BOOSTS_CHANGE_PRIVACY,
|
||||
} from 'flavours/glitch/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;
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import status_lists from './status_lists';
|
||||
import mutes from './mutes';
|
||||
import blocks from './blocks';
|
||||
import reports from './reports';
|
||||
import boosts from './boosts';
|
||||
import contexts from './contexts';
|
||||
import compose from './compose';
|
||||
import search from './search';
|
||||
@ -61,6 +62,7 @@ const reducers = {
|
||||
mutes,
|
||||
blocks,
|
||||
reports,
|
||||
boosts,
|
||||
contexts,
|
||||
compose,
|
||||
search,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
|
||||
import { TIMELINE_DELETE } from 'flavours/glitch/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;
|
||||
}
|
||||
|
@ -1031,3 +1031,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-root__container .composer--options--dropdown {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.modal-root__container .composer--options--dropdown--content {
|
||||
pointer-events: auto;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import EXIF from 'exif-js';
|
||||
|
||||
const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
|
||||
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
|
||||
|
||||
const _browser_quirks = {};
|
||||
|
||||
|
29
app/javascript/mastodon/actions/boosts.js
Normal file
29
app/javascript/mastodon/actions/boosts.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
@ -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({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
menu.push(null);
|
||||
|
||||
if (status.getIn(['account', 'id']) === me || withDismiss) {
|
||||
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 (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>
|
||||
|
25
app/javascript/mastodon/reducers/boosts.js
Normal file
25
app/javascript/mastodon/reducers/boosts.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
15
lib/action_dispatch/cookie_jar_extensions.rb
Normal file
15
lib/action_dispatch/cookie_jar_extensions.rb
Normal 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
lib/rails/engine_extensions.rb
Normal file
11
lib/rails/engine_extensions.rb
Normal 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)
|
30
package.json
30
package.json
@ -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
|
||||
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 'does not create a follow request' do
|
||||
expect(sender.requested?(recipient)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a follow request' do
|
||||
expect(sender.requested?(recipient)).to be true
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 'unlocked account muting the sender' do
|
||||
context 'when a follow request already exists' do
|
||||
before do
|
||||
recipient.mute!(sender)
|
||||
subject.perform
|
||||
sender.follow_requests.create!(target_account: recipient, uri: 'bar')
|
||||
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 '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 '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
|
||||
|
||||
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 '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
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user