diff --git a/.codeclimate.yml b/.codeclimate.yml index 62dc2334ca..8701e5f3d2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -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: diff --git a/Gemfile b/Gemfile index 639a477298..fc38339c96 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 7d1a467dad..aa8cba1f6f 100644 --- a/Gemfile.lock +++ b/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) diff --git a/Vagrantfile b/Vagrantfile index 143fa27fdb..addc6b0f4a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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 diff --git a/app/controllers/api/web/settings_controller.rb b/app/controllers/api/web/settings_controller.rb index 3d65e46ed9..601e25e6ee 100644 --- a/app/controllers/api/web/settings_controller.rb +++ b/app/controllers/api/web/settings_controller.rb @@ -2,17 +2,16 @@ class Api::Web::SettingsController < Api::Web::BaseController before_action :require_user! + before_action :set_setting def update - setting.data = params[:data] - setting.save! - + @setting.update!(data: params[:data]) render_empty end private - def setting - @_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user) + def set_setting + @setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41fe9d88a4..a4b740b893 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -44,7 +44,7 @@ class ApplicationController < ActionController::Base private def https_enabled? - Rails.env.production? && !request.path.start_with?('/health') + Rails.env.production? && !request.path.start_with?('/health') && !request.headers["Host"].ends_with?(".onion") end def authorized_fetch_mode? diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 4b074ca192..b3b5476e2f 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController private def set_account - @account = Account.find(-99) + @account = Account.representative end def restrict_fields_to diff --git a/app/helpers/mascot_helper.rb b/app/helpers/mascot_helper.rb new file mode 100644 index 0000000000..0124c74f19 --- /dev/null +++ b/app/helpers/mascot_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module MascotHelper + def mascot_url + full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg')) + end + + private + + def instance_presenter + @instance_presenter ||= InstancePresenter.new + end +end diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js new file mode 100644 index 0000000000..6e14065d6f --- /dev/null +++ b/app/javascript/flavours/glitch/actions/boosts.js @@ -0,0 +1,29 @@ +import { openModal } from './modal'; + +export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL'; +export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY'; + +export function initBoostModal(props) { + return (dispatch, getState) => { + const default_privacy = getState().getIn(['compose', 'default_privacy']); + + const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy; + + dispatch({ + type: BOOSTS_INIT_MODAL, + privacy + }); + + dispatch(openModal('BOOST', props)); + }; +} + + +export function changeBoostPrivacy(privacy) { + return dispatch => { + dispatch({ + type: BOOSTS_CHANGE_PRIVACY, + privacy, + }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index 4407f8b6ea..336c8fa907 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST'; export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS'; export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL'; -export function reblog(status) { +export function reblog(status, visibility) { return function (dispatch, getState) { dispatch(reblogRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { + api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper dispatch(importFetchedStatus(response.data.reblog)); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index d1aba691ca..023fecb9ab 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent { disabled: PropTypes.bool, status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, onOpen: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, dropdownPlacement: PropTypes.string, diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 2ccb02c624..74bfd948ed 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -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 { diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index e60ee814d8..1c0385b740 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -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']), diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 7782246a60..6461bf8050 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -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 })); } }); }, diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 04ef3964b2..abf7cbba14 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -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} > , - name: 'direct', - text: , - }, - private: { - icon: 'lock', - meta: , - name: 'private', - text: , - }, - public: { - icon: 'globe', - meta: , - name: 'public', - text: , - }, - unlisted: { - icon: 'unlock', - meta: , - name: 'unlisted', - text: , - }, - }; - const contentTypeItems = { plain: { icon: 'file-text', @@ -297,19 +233,11 @@ class ComposerOptions extends ImmutablePureComponent { /> )}
- {showContentTypeChoice && ( diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js new file mode 100644 index 0000000000..39f7c7bd16 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -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: , + name: 'direct', + text: , + }, + private: { + icon: 'lock', + meta: , + name: 'private', + text: , + }, + public: { + icon: 'globe', + meta: , + name: 'public', + text: , + }, + unlisted: { + icon: 'unlock', + meta: , + name: 'unlisted', + text: , + }, + }; + + const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private]; + + if (!noDirect) { + items.push(privacyItems.direct); + } + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js index 2ddba140e2..d8989ec61b 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -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 })); } }; diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 0f16d93fe3..6ed5f38659 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -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 }); diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index 9d11f37e05..40e186569e 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -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 })); } } }, diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index b330adf3ff..21e4414070 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -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 })); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js index 12ad426c84..c4af25599a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -10,7 +11,9 @@ import DisplayName from '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 { Shift + }} /> } + + {status.get('visibility') !== 'private' && !status.get('reblogged') && ( + + )}