Merge pull request #1500 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changesremotes/1727458204337373841/tmp_refs/heads/signup-info-prompt
commit
8792128f38
|
@ -30,7 +30,7 @@ plugins:
|
||||||
channel: eslint-7
|
channel: eslint-7
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
channel: rubocop-1-8-1
|
channel: rubocop-1-9-1
|
||||||
sass-lint:
|
sass-lint:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
|
|
8
Gemfile
8
Gemfile
|
@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.7'
|
gem 'pghero', '~> 2.7'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.87', require: false
|
gem 'aws-sdk-s3', '~> 1.88', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
|
@ -27,7 +27,7 @@ gem 'blurhash', '~> 0.1'
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.7'
|
gem 'addressable', '~> 2.7'
|
||||||
gem 'bootsnap', '~> 1.5', require: false
|
gem 'bootsnap', '~> 1.6.0', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
|
@ -73,7 +73,7 @@ gem 'parallel', '~> 1.20'
|
||||||
gem 'posix-spawn'
|
gem 'posix-spawn'
|
||||||
gem 'pundit', '~> 2.1'
|
gem 'pundit', '~> 2.1'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'rack-attack', '~> 6.4'
|
gem 'rack-attack', '~> 6.5'
|
||||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 5.1'
|
gem 'rails-i18n', '~> 5.1'
|
||||||
gem 'rails-settings-cached', '~> 0.6'
|
gem 'rails-settings-cached', '~> 0.6'
|
||||||
|
@ -140,7 +140,7 @@ group :development do
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 1.4'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 1.8', require: false
|
gem 'rubocop', '~> 1.9', require: false
|
||||||
gem 'rubocop-rails', '~> 2.9', require: false
|
gem 'rubocop-rails', '~> 2.9', require: false
|
||||||
gem 'brakeman', '~> 4.10', require: false
|
gem 'brakeman', '~> 4.10', require: false
|
||||||
gem 'bundler-audit', '~> 0.7', require: false
|
gem 'bundler-audit', '~> 0.7', require: false
|
||||||
|
|
44
Gemfile.lock
44
Gemfile.lock
|
@ -72,24 +72,24 @@ GEM
|
||||||
activerecord (>= 3.2, < 7.0)
|
activerecord (>= 3.2, < 7.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
arel (9.0.0)
|
arel (9.0.0)
|
||||||
ast (2.4.1)
|
ast (2.4.2)
|
||||||
attr_encrypted (3.1.0)
|
attr_encrypted (3.1.0)
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.413.0)
|
aws-partitions (1.424.0)
|
||||||
aws-sdk-core (3.110.0)
|
aws-sdk-core (3.112.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.40.0)
|
aws-sdk-kms (1.42.0)
|
||||||
aws-sdk-core (~> 3, >= 3.109.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.87.0)
|
aws-sdk-s3 (1.88.0)
|
||||||
aws-sdk-core (~> 3, >= 3.109.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.2)
|
aws-sigv4 (1.2.2)
|
||||||
|
@ -104,7 +104,7 @@ GEM
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.4)
|
blurhash (0.1.4)
|
||||||
ffi (~> 1.10.0)
|
ffi (~> 1.10.0)
|
||||||
bootsnap (1.5.1)
|
bootsnap (1.6.0)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (4.10.1)
|
brakeman (4.10.1)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
|
@ -275,7 +275,7 @@ GEM
|
||||||
httplog (1.4.3)
|
httplog (1.4.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.7)
|
i18n (1.8.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.33)
|
i18n-tasks (0.9.33)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
|
@ -293,7 +293,7 @@ GEM
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
json (2.3.1)
|
json (2.3.1)
|
||||||
json-canonicalization (0.2.0)
|
json-canonicalization (0.2.0)
|
||||||
json-ld (3.1.7)
|
json-ld (3.1.8)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 0.2)
|
json-canonicalization (~> 0.2)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
|
@ -354,7 +354,7 @@ GEM
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.5.0)
|
mini_portile2 (2.5.0)
|
||||||
minitest (5.14.3)
|
minitest (5.14.3)
|
||||||
msgpack (1.3.3)
|
msgpack (1.4.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-ldap (0.17.0)
|
net-ldap (0.17.0)
|
||||||
|
@ -408,9 +408,9 @@ GEM
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pghero (2.7.3)
|
pghero (2.7.4)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.4)
|
pkg-config (1.4.5)
|
||||||
pluck_each (0.1.3)
|
pluck_each (0.1.3)
|
||||||
activerecord (> 3.2.0)
|
activerecord (> 3.2.0)
|
||||||
activesupport (> 3.0.0)
|
activesupport (> 3.0.0)
|
||||||
|
@ -439,7 +439,7 @@ GEM
|
||||||
raabro (1.3.3)
|
raabro (1.3.3)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-attack (6.4.0)
|
rack-attack (6.5.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
|
@ -482,7 +482,7 @@ GEM
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (13.0.3)
|
rake (13.0.3)
|
||||||
rdf (3.1.8)
|
rdf (3.1.10)
|
||||||
hamster (~> 3.0)
|
hamster (~> 3.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
|
@ -496,7 +496,7 @@ GEM
|
||||||
redis-activesupport (5.2.0)
|
redis-activesupport (5.2.0)
|
||||||
activesupport (>= 3, < 7)
|
activesupport (>= 3, < 7)
|
||||||
redis-store (>= 1.3, < 2)
|
redis-store (>= 1.3, < 2)
|
||||||
redis-namespace (1.8.0)
|
redis-namespace (1.8.1)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
redis-rack (2.1.3)
|
redis-rack (2.1.3)
|
||||||
rack (>= 2.0.8, < 3)
|
rack (>= 2.0.8, < 3)
|
||||||
|
@ -542,7 +542,7 @@ GEM
|
||||||
rspec-support (3.10.1)
|
rspec-support (3.10.1)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.8.1)
|
rubocop (1.9.1)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.0.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
|
@ -551,7 +551,7 @@ GEM
|
||||||
rubocop-ast (>= 1.2.0, < 2.0)
|
rubocop-ast (>= 1.2.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.4.0)
|
rubocop-ast (1.4.1)
|
||||||
parser (>= 2.7.1.5)
|
parser (>= 2.7.1.5)
|
||||||
rubocop-rails (2.9.1)
|
rubocop-rails (2.9.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
|
@ -693,11 +693,11 @@ DEPENDENCIES
|
||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.87)
|
aws-sdk-s3 (~> 1.88)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.5)
|
bootsnap (~> 1.6.0)
|
||||||
brakeman (~> 4.10)
|
brakeman (~> 4.10)
|
||||||
browser
|
browser
|
||||||
bullet (~> 6.1)
|
bullet (~> 6.1)
|
||||||
|
@ -777,7 +777,7 @@ DEPENDENCIES
|
||||||
puma (~> 5.1)
|
puma (~> 5.1)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.4)
|
rack-attack (~> 6.5)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
rails (~> 5.2.4.4)
|
rails (~> 5.2.4.4)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
|
@ -792,7 +792,7 @@ DEPENDENCIES
|
||||||
rspec-rails (~> 4.0)
|
rspec-rails (~> 4.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.4)
|
||||||
rubocop (~> 1.8)
|
rubocop (~> 1.9)
|
||||||
rubocop-rails (~> 2.9)
|
rubocop-rails (~> 2.9)
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 5.2)
|
sanitize (~> 5.2)
|
||||||
|
|
|
@ -72,10 +72,12 @@ bundle install
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Build Mastodon
|
# Build Mastodon
|
||||||
|
export RAILS_ENV=development
|
||||||
export $(cat ".env.vagrant" | xargs)
|
export $(cat ".env.vagrant" | xargs)
|
||||||
bundle exec rails db:setup
|
bundle exec rails db:setup
|
||||||
|
|
||||||
# Configure automatic loading of environment variable
|
# Configure automatic loading of environment variable
|
||||||
|
echo 'export RAILS_ENV=development' >> ~/.bash_profile
|
||||||
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
|
echo 'export $(cat "/vagrant/.env.vagrant" | xargs)' >> ~/.bash_profile
|
||||||
|
|
||||||
SCRIPT
|
SCRIPT
|
||||||
|
|
|
@ -2,17 +2,16 @@
|
||||||
|
|
||||||
class Api::Web::SettingsController < Api::Web::BaseController
|
class Api::Web::SettingsController < Api::Web::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
before_action :set_setting
|
||||||
|
|
||||||
def update
|
def update
|
||||||
setting.data = params[:data]
|
@setting.update!(data: params[:data])
|
||||||
setting.save!
|
|
||||||
|
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def setting
|
def set_setting
|
||||||
@_setting ||= ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
|
@setting = ::Web::Setting.where(user: current_user).first_or_initialize(user: current_user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ class ApplicationController < ActionController::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def https_enabled?
|
def https_enabled?
|
||||||
Rails.env.production? && !request.path.start_with?('/health')
|
Rails.env.production? && !request.path.start_with?('/health') && !request.headers["Host"].ends_with?(".onion")
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_fetch_mode?
|
def authorized_fetch_mode?
|
||||||
|
|
|
@ -13,7 +13,7 @@ class InstanceActorsController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find(-99)
|
@account = Account.representative
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
def restrict_fields_to
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module MascotHelper
|
||||||
|
def mascot_url
|
||||||
|
full_asset_url(instance_presenter.mascot&.file&.url || asset_pack_path('media/images/elephant_ui_plane.svg'))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def instance_presenter
|
||||||
|
@instance_presenter ||= InstancePresenter.new
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
|
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
|
||||||
|
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
|
||||||
|
|
||||||
|
export function initBoostModal(props) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const default_privacy = getState().getIn(['compose', 'default_privacy']);
|
||||||
|
|
||||||
|
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: BOOSTS_INIT_MODAL,
|
||||||
|
privacy
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('BOOST', props));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function changeBoostPrivacy(privacy) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: BOOSTS_CHANGE_PRIVACY,
|
||||||
|
privacy,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
||||||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||||
|
|
||||||
export function reblog(status) {
|
export function reblog(status, visibility) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(reblogRequest(status));
|
dispatch(reblogRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
|
||||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||||
dispatch(importFetchedStatus(response.data.reblog));
|
dispatch(importFetchedStatus(response.data.reblog));
|
||||||
|
|
|
@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
isUserTouching: PropTypes.func,
|
isUserTouching: PropTypes.func,
|
||||||
isModalOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpen: PropTypes.func.isRequired,
|
onOpen: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
dropdownPlacement: PropTypes.string,
|
dropdownPlacement: PropTypes.string,
|
||||||
|
|
|
@ -193,9 +193,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props;
|
const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props;
|
||||||
|
|
||||||
const mutingConversation = status.get('muted');
|
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
|
const mutingConversation = status.get('muted');
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
|
@ -211,16 +212,17 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
menu.push(null);
|
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({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.getIn(['account', 'id']) === me) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
|
||||||
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
|
import { isUserTouching } from 'flavours/glitch/util/is_mobile';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
isModalOpen: state.get('modal').modalType === 'ACTIONS',
|
|
||||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
|
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
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')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -108,11 +109,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = 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')) {
|
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) {
|
} else if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,6 +31,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
noModal: PropTypes.bool,
|
||||||
|
container: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -42,10 +44,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||||
|
|
||||||
// Toggles opening and closing the dropdown.
|
// Toggles opening and closing the dropdown.
|
||||||
handleToggle = ({ target, type }) => {
|
handleToggle = ({ target, type }) => {
|
||||||
const { onModalOpen } = this.props;
|
const { onModalOpen, noModal } = this.props;
|
||||||
const { open } = this.state;
|
const { open } = this.state;
|
||||||
|
|
||||||
if (isUserTouching()) {
|
if (!noModal && isUserTouching()) {
|
||||||
if (this.state.open) {
|
if (this.state.open) {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,6 +185,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||||
items,
|
items,
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
|
container,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { open, placement } = this.state;
|
const { open, placement } = this.state;
|
||||||
const computedClass = classNames('composer--options--dropdown', {
|
const computedClass = classNames('composer--options--dropdown', {
|
||||||
|
@ -219,6 +222,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
||||||
placement={placement}
|
placement={placement}
|
||||||
show={open}
|
show={open}
|
||||||
target={this}
|
target={this}
|
||||||
|
container={container}
|
||||||
>
|
>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
items={items}
|
items={items}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import spring from 'react-motion/lib/spring';
|
||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import TextIconButton from './text_icon_button';
|
import TextIconButton from './text_icon_button';
|
||||||
import Dropdown from './dropdown';
|
import Dropdown from './dropdown';
|
||||||
|
import PrivacyDropdown from './privacy_dropdown';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
// Utils.
|
// Utils.
|
||||||
|
@ -25,22 +26,10 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Attach...',
|
defaultMessage: 'Attach...',
|
||||||
id: 'compose.attach',
|
id: 'compose.attach',
|
||||||
},
|
},
|
||||||
change_privacy: {
|
|
||||||
defaultMessage: 'Adjust status privacy',
|
|
||||||
id: 'privacy.change',
|
|
||||||
},
|
|
||||||
content_type: {
|
content_type: {
|
||||||
defaultMessage: 'Content type',
|
defaultMessage: 'Content type',
|
||||||
id: 'content-type.change',
|
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: {
|
doodle: {
|
||||||
defaultMessage: 'Draw something',
|
defaultMessage: 'Draw something',
|
||||||
id: 'compose.attach.doodle',
|
id: 'compose.attach.doodle',
|
||||||
|
@ -65,22 +54,6 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Plain text',
|
defaultMessage: 'Plain text',
|
||||||
id: 'compose.content-type.plain',
|
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: {
|
spoiler: {
|
||||||
defaultMessage: 'Hide text behind warning',
|
defaultMessage: 'Hide text behind warning',
|
||||||
id: 'compose_form.spoiler',
|
id: 'compose_form.spoiler',
|
||||||
|
@ -93,14 +66,6 @@ const messages = defineMessages({
|
||||||
defaultMessage: 'Threaded mode',
|
defaultMessage: 'Threaded mode',
|
||||||
id: 'advanced_options.threaded_mode.short',
|
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: {
|
upload: {
|
||||||
defaultMessage: 'Upload a file',
|
defaultMessage: 'Upload a file',
|
||||||
id: 'compose.attach.upload',
|
id: 'compose.attach.upload',
|
||||||
|
@ -201,35 +166,6 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||||
showContentTypeChoice,
|
showContentTypeChoice,
|
||||||
} = this.props;
|
} = 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 = {
|
const contentTypeItems = {
|
||||||
plain: {
|
plain: {
|
||||||
icon: 'file-text',
|
icon: 'file-text',
|
||||||
|
@ -297,19 +233,11 @@ class ComposerOptions extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
<Dropdown
|
<PrivacyDropdown
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={(privacyItems[privacy] || {}).icon}
|
|
||||||
items={[
|
|
||||||
privacyItems.public,
|
|
||||||
privacyItems.unlisted,
|
|
||||||
privacyItems.private,
|
|
||||||
privacyItems.direct,
|
|
||||||
]}
|
|
||||||
onChange={onChangeVisibility}
|
onChange={onChangeVisibility}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
onModalOpen={onModalOpen}
|
onModalOpen={onModalOpen}
|
||||||
title={intl.formatMessage(messages.change_privacy)}
|
|
||||||
value={privacy}
|
value={privacy}
|
||||||
/>
|
/>
|
||||||
{showContentTypeChoice && (
|
{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 { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
|
@ -82,9 +83,9 @@ class Footer extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_performReblog = () => {
|
_performReblog = (privacy) => {
|
||||||
const { dispatch, status } = this.props;
|
const { dispatch, status } = this.props;
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = e => {
|
handleReblogClick = e => {
|
||||||
|
@ -95,7 +96,7 @@ class Footer extends ImmutablePureComponent {
|
||||||
} else if ((e && e.shiftKey) || !boostModal) {
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
this._performReblog();
|
this._performReblog();
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
|
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -145,8 +145,9 @@ class ActionBar extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { status, intl } = this.props;
|
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 mutingConversation = status.get('muted');
|
||||||
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -156,12 +157,12 @@ class ActionBar extends React.PureComponent {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me === status.getIn(['account', 'id'])) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
|
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
|
||||||
|
@ -67,8 +68,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalReblog (status) {
|
onModalReblog (status, privacy) {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
|
@ -78,7 +79,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (e.shiftKey || !boostModal) {
|
if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
import { initReport } from 'flavours/glitch/actions/reports';
|
import { initReport } from 'flavours/glitch/actions/reports';
|
||||||
|
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||||
import { makeGetStatus } from 'flavours/glitch/selectors';
|
import { makeGetStatus } from 'flavours/glitch/selectors';
|
||||||
import { ScrollContainer } from 'react-router-scroll-4';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
|
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;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,11 +277,11 @@ class Status extends ImmutablePureComponent {
|
||||||
const { settings, dispatch } = this.props;
|
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')) {
|
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) {
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
this.handleModalReblog(status);
|
this.handleModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -10,7 +11,9 @@ import DisplayName from 'flavours/glitch/components/display_name';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import Icon from 'flavours/glitch/components/icon';
|
import Icon from 'flavours/glitch/components/icon';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
|
@ -21,7 +24,22 @@ const messages = defineMessages({
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
privacy: state.getIn(['boosts', 'new', 'privacy']),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
onChangeBoostPrivacy(value) {
|
||||||
|
dispatch(changeBoostPrivacy(value));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
class BoostModal extends ImmutablePureComponent {
|
class BoostModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -41,7 +59,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblog = () => {
|
handleReblog = () => {
|
||||||
this.props.onReblog(this.props.status);
|
this.props.onReblog(this.props.status, this.props.privacy);
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,12 +73,16 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findContainer = () => {
|
||||||
|
return document.getElementsByClassName('modal-root__container')[0];
|
||||||
|
};
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
this.button = c;
|
this.button = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, missingMediaDescription, intl } = this.props;
|
const { status, missingMediaDescription, privacy, intl } = this.props;
|
||||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||||
|
|
||||||
const visibilityIconInfo = {
|
const visibilityIconInfo = {
|
||||||
|
@ -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> }} />
|
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
|
||||||
|
<PrivacyDropdown
|
||||||
|
noDirect
|
||||||
|
value={privacy}
|
||||||
|
container={this._findContainer}
|
||||||
|
onChange={this.props.onChangeBoostPrivacy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BOOSTS_INIT_MODAL,
|
||||||
|
BOOSTS_CHANGE_PRIVACY,
|
||||||
|
} from '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 mutes from './mutes';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import reports from './reports';
|
import reports from './reports';
|
||||||
|
import boosts from './boosts';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
|
@ -61,6 +62,7 @@ const reducers = {
|
||||||
mutes,
|
mutes,
|
||||||
blocks,
|
blocks,
|
||||||
reports,
|
reports,
|
||||||
|
boosts,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
search,
|
search,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
|
import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
|
||||||
|
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
modalType: null,
|
modalType: null,
|
||||||
|
@ -11,6 +12,8 @@ export default function modal(state = initialState, action) {
|
||||||
return { modalType: action.modalType, modalProps: action.modalProps };
|
return { modalType: action.modalType, modalProps: action.modalProps };
|
||||||
case MODAL_CLOSE:
|
case MODAL_CLOSE:
|
||||||
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
|
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
|
||||||
|
case TIMELINE_DELETE:
|
||||||
|
return (state.modalProps.statusId === action.id) ? initialState : state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
> * {
|
> div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 235px;
|
max-height: 235px;
|
||||||
background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain;
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import EXIF from 'exif-js';
|
import EXIF from 'exif-js';
|
||||||
|
|
||||||
const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
|
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
|
||||||
|
|
||||||
const _browser_quirks = {};
|
const _browser_quirks = {};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
|
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
|
||||||
|
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
|
||||||
|
|
||||||
|
export function initBoostModal(props) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const default_privacy = getState().getIn(['compose', 'default_privacy']);
|
||||||
|
|
||||||
|
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: BOOSTS_INIT_MODAL,
|
||||||
|
privacy
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('BOOST', props));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function changeBoostPrivacy(privacy) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: BOOSTS_CHANGE_PRIVACY,
|
||||||
|
privacy,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -41,11 +41,11 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
||||||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||||
|
|
||||||
export function reblog(status) {
|
export function reblog(status, visibility) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(reblogRequest(status));
|
dispatch(reblogRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
|
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`, { visibility }).then(function (response) {
|
||||||
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
// The reblog API method returns a new status wrapped around the original. In this case we are only
|
||||||
// interested in how the original is modified, hence passing it skipping the wrapper
|
// interested in how the original is modified, hence passing it skipping the wrapper
|
||||||
dispatch(importFetchedStatus(response.data.reblog));
|
dispatch(importFetchedStatus(response.data.reblog));
|
||||||
|
|
|
@ -177,7 +177,6 @@ export default class Dropdown extends React.PureComponent {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
isUserTouching: PropTypes.func,
|
isUserTouching: PropTypes.func,
|
||||||
isModalOpen: PropTypes.bool.isRequired,
|
|
||||||
onOpen: PropTypes.func.isRequired,
|
onOpen: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
dropdownPlacement: PropTypes.string,
|
dropdownPlacement: PropTypes.string,
|
||||||
|
|
|
@ -223,10 +223,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
||||||
|
|
||||||
const mutingConversation = status.get('muted');
|
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -237,19 +238,22 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
|
||||||
menu.push(null);
|
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({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.getIn(['account', 'id']) === me) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import DropdownMenu from '../components/dropdown_menu';
|
||||||
import { isUserTouching } from '../is_mobile';
|
import { isUserTouching } from '../is_mobile';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
isModalOpen: state.get('modal').modalType === 'ACTIONS',
|
|
||||||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
|
||||||
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
|
||||||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
} from '../actions/domain_blocks';
|
} from '../actions/domain_blocks';
|
||||||
import { initMuteModal } from '../actions/mutes';
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import { initBlockModal } from '../actions/blocks';
|
import { initBlockModal } from '../actions/blocks';
|
||||||
|
import { initBoostModal } from '../actions/boosts';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { deployPictureInPicture } from '../actions/picture_in_picture';
|
import { deployPictureInPicture } from '../actions/picture_in_picture';
|
||||||
|
@ -82,11 +83,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalReblog (status) {
|
onModalReblog (status, privacy) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -94,7 +95,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if ((e && e.shiftKey) || !boostModal) {
|
if ((e && e.shiftKey) || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||||
// It should not be transformed when mounting because the resulting
|
// It should not be transformed when mounting because the resulting
|
||||||
// size will be used to determine the coordinate of the menu by
|
// size will be used to determine the coordinate of the menu by
|
||||||
// react-overlays
|
// react-overlays
|
||||||
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, zIndex: 2 }} role='listbox' ref={this.setRef}>
|
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||||
<div className='privacy-dropdown__option__icon'>
|
<div className='privacy-dropdown__option__icon'>
|
||||||
|
@ -153,11 +153,12 @@ class PrivacyDropdown extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isUserTouching: PropTypes.func,
|
isUserTouching: PropTypes.func,
|
||||||
isModalOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalOpen: PropTypes.func,
|
onModalOpen: PropTypes.func,
|
||||||
onModalClose: PropTypes.func,
|
onModalClose: PropTypes.func,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
noDirect: PropTypes.bool,
|
||||||
|
container: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ class PrivacyDropdown extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggle = ({ target }) => {
|
handleToggle = ({ target }) => {
|
||||||
if (this.props.isUserTouching()) {
|
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||||
if (this.state.open) {
|
if (this.state.open) {
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
} else {
|
} else {
|
||||||
|
@ -236,12 +237,17 @@ class PrivacyDropdown extends React.PureComponent {
|
||||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||||
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!this.props.noDirect) {
|
||||||
|
this.options.push(
|
||||||
|
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, intl } = this.props;
|
const { value, container, intl } = this.props;
|
||||||
const { open, placement } = this.state;
|
const { open, placement } = this.state;
|
||||||
|
|
||||||
const valueOption = this.options.find(item => item.value === value);
|
const valueOption = this.options.find(item => item.value === value);
|
||||||
|
@ -264,7 +270,7 @@ class PrivacyDropdown extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Overlay show={open} placement={placement} target={this}>
|
<Overlay show={open} placement={placement} target={this} container={container}>
|
||||||
<PrivacyDropdownMenu
|
<PrivacyDropdownMenu
|
||||||
items={this.options}
|
items={this.options}
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { openModal, closeModal } from '../../../actions/modal';
|
||||||
import { isUserTouching } from '../../../is_mobile';
|
import { isUserTouching } from '../../../is_mobile';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
isModalOpen: state.get('modal').modalType === 'ACTIONS',
|
|
||||||
value: state.getIn(['compose', 'privacy']),
|
value: state.getIn(['compose', 'privacy']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeGetNotification, makeGetStatus } from '../../../selectors';
|
import { makeGetNotification, makeGetStatus } from '../../../selectors';
|
||||||
import Notification from '../components/notification';
|
import Notification from '../components/notification';
|
||||||
|
import { initBoostModal } from '../../../actions/boosts';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
import {
|
import {
|
||||||
|
@ -35,8 +36,8 @@ const mapDispatchToProps = dispatch => ({
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account, router));
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalReblog (status) {
|
onModalReblog (status, privacy) {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
|
@ -46,7 +47,7 @@ const mapDispatchToProps = dispatch => ({
|
||||||
if (e.shiftKey || !boostModal) {
|
if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import { replyCompose } from 'mastodon/actions/compose';
|
import { replyCompose } from 'mastodon/actions/compose';
|
||||||
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
||||||
import { makeGetStatus } from 'mastodon/selectors';
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
|
import { initBoostModal } from 'mastodon/actions/boosts';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -89,9 +90,9 @@ class Footer extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_performReblog = () => {
|
_performReblog = (status, privacy) => {
|
||||||
const { dispatch, status } = this.props;
|
const { dispatch } = this.props;
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = e => {
|
handleReblogClick = e => {
|
||||||
|
@ -100,9 +101,9 @@ class Footer extends ImmutablePureComponent {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else if ((e && e.shiftKey) || !boostModal) {
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
this._performReblog();
|
this._performReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
|
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -187,9 +187,10 @@ class ActionBar extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { status, relationship, intl } = this.props;
|
const { status, relationship, intl } = this.props;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
@ -199,12 +200,12 @@ class ActionBar extends React.PureComponent {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me === status.getIn(['account', 'id'])) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
if (publicStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from '../../../actions/statuses';
|
} from '../../../actions/statuses';
|
||||||
import { initMuteModal } from '../../../actions/mutes';
|
import { initMuteModal } from '../../../actions/mutes';
|
||||||
import { initBlockModal } from '../../../actions/blocks';
|
import { initBlockModal } from '../../../actions/blocks';
|
||||||
|
import { initBoostModal } from '../../../actions/boosts';
|
||||||
import { initReport } from '../../../actions/reports';
|
import { initReport } from '../../../actions/reports';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
@ -68,8 +69,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalReblog (status) {
|
onModalReblog (status, privacy) {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status, privacy));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
|
@ -79,7 +80,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (e.shiftKey || !boostModal) {
|
if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,6 +42,7 @@ import {
|
||||||
} from '../../actions/domain_blocks';
|
} from '../../actions/domain_blocks';
|
||||||
import { initMuteModal } from '../../actions/mutes';
|
import { initMuteModal } from '../../actions/mutes';
|
||||||
import { initBlockModal } from '../../actions/blocks';
|
import { initBlockModal } from '../../actions/blocks';
|
||||||
|
import { initBoostModal } from '../../actions/boosts';
|
||||||
import { initReport } from '../../actions/reports';
|
import { initReport } from '../../actions/reports';
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
|
||||||
import { ScrollContainer } from 'react-router-scroll-4';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
|
@ -234,8 +235,8 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleModalReblog = (status) => {
|
handleModalReblog = (status, privacy) => {
|
||||||
this.props.dispatch(reblog(status));
|
this.props.dispatch(reblog(status, privacy));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblogClick = (status, e) => {
|
handleReblogClick = (status, e) => {
|
||||||
|
@ -245,7 +246,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if ((e && e.shiftKey) || !boostModal) {
|
if ((e && e.shiftKey) || !boostModal) {
|
||||||
this.handleModalReblog(status);
|
this.handleModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -10,7 +11,9 @@ import DisplayName from '../../../components/display_name';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import AttachmentList from 'mastodon/components/attachment_list';
|
import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
|
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { changeBoostPrivacy } from 'mastodon/actions/boosts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
|
@ -21,7 +24,22 @@ const messages = defineMessages({
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
privacy: state.getIn(['boosts', 'new', 'privacy']),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
onChangeBoostPrivacy(value) {
|
||||||
|
dispatch(changeBoostPrivacy(value));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
class BoostModal extends ImmutablePureComponent {
|
class BoostModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -32,6 +50,8 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
onReblog: PropTypes.func.isRequired,
|
onReblog: PropTypes.func.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onChangeBoostPrivacy: PropTypes.func.isRequired,
|
||||||
|
privacy: PropTypes.string.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,7 +60,7 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReblog = () => {
|
handleReblog = () => {
|
||||||
this.props.onReblog(this.props.status);
|
this.props.onReblog(this.props.status, this.props.privacy);
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,12 +72,16 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_findContainer = () => {
|
||||||
|
return document.getElementsByClassName('modal-root__container')[0];
|
||||||
|
};
|
||||||
|
|
||||||
setRef = (c) => {
|
setRef = (c) => {
|
||||||
this.button = c;
|
this.button = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl } = this.props;
|
const { status, privacy, intl } = this.props;
|
||||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||||
|
|
||||||
const visibilityIconInfo = {
|
const visibilityIconInfo = {
|
||||||
|
@ -102,6 +126,14 @@ class BoostModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='boost-modal__action-bar'>
|
<div className='boost-modal__action-bar'>
|
||||||
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
|
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
|
||||||
|
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
|
||||||
|
<PrivacyDropdown
|
||||||
|
noDirect
|
||||||
|
value={privacy}
|
||||||
|
container={this._findContainer}
|
||||||
|
onChange={this.props.onChangeBoostPrivacy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} ref={this.setRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BOOSTS_INIT_MODAL,
|
||||||
|
BOOSTS_CHANGE_PRIVACY,
|
||||||
|
} from 'mastodon/actions/boosts';
|
||||||
|
|
||||||
|
const initialState = Immutable.Map({
|
||||||
|
new: Immutable.Map({
|
||||||
|
privacy: 'public',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function mutes(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case BOOSTS_INIT_MODAL:
|
||||||
|
return state.withMutations((state) => {
|
||||||
|
state.setIn(['new', 'privacy'], action.privacy);
|
||||||
|
});
|
||||||
|
case BOOSTS_CHANGE_PRIVACY:
|
||||||
|
return state.setIn(['new', 'privacy'], action.privacy);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import push_notifications from './push_notifications';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
|
import boosts from './boosts';
|
||||||
import reports from './reports';
|
import reports from './reports';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
|
@ -57,6 +58,7 @@ const reducers = {
|
||||||
push_notifications,
|
push_notifications,
|
||||||
mutes,
|
mutes,
|
||||||
blocks,
|
blocks,
|
||||||
|
boosts,
|
||||||
reports,
|
reports,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
|
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal';
|
||||||
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
modalType: null,
|
modalType: null,
|
||||||
|
@ -11,6 +12,8 @@ export default function modal(state = initialState, action) {
|
||||||
return { modalType: action.modalType, modalProps: action.modalProps };
|
return { modalType: action.modalType, modalProps: action.modalProps };
|
||||||
case MODAL_CLOSE:
|
case MODAL_CLOSE:
|
||||||
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
|
return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state;
|
||||||
|
case TIMELINE_DELETE:
|
||||||
|
return (state.modalProps.statusId === action.id) ? initialState : state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import EXIF from 'exif-js';
|
import EXIF from 'exif-js';
|
||||||
|
|
||||||
const MAX_IMAGE_PIXELS = 1638400; // 1280x1280px
|
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
|
||||||
|
|
||||||
const _browser_quirks = {};
|
const _browser_quirks = {};
|
||||||
|
|
||||||
|
|
|
@ -4209,6 +4209,7 @@ a.status-card.compact:hover {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
&.top {
|
&.top {
|
||||||
transform-origin: 50% 100%;
|
transform-origin: 50% 100%;
|
||||||
|
@ -4219,6 +4220,15 @@ a.status-card.compact:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-root__container .privacy-dropdown {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-root__container .privacy-dropdown__dropdown {
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
.privacy-dropdown__option {
|
.privacy-dropdown__option {
|
||||||
color: $inverted-text-color;
|
color: $inverted-text-color;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -12,10 +12,19 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
> * {
|
> div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 235px;
|
max-height: 235px;
|
||||||
background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain;
|
position: relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,14 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||||
def perform
|
def perform
|
||||||
target_account = account_from_uri(object_uri)
|
target_account = account_from_uri(object_uri)
|
||||||
|
|
||||||
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
|
return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id'])
|
||||||
|
|
||||||
|
# Update id of already-existing follow requests
|
||||||
|
existing_follow_request = ::FollowRequest.find_by(account: @account, target_account: target_account)
|
||||||
|
unless existing_follow_request.nil?
|
||||||
|
existing_follow_request.update!(uri: @json['id'])
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
|
if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
|
||||||
reject_follow_request!(target_account)
|
reject_follow_request!(target_account)
|
||||||
|
@ -14,7 +21,9 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fast-forward repeat follow requests
|
# Fast-forward repeat follow requests
|
||||||
if @account.following?(target_account)
|
existing_follow = ::Follow.find_by(account: @account, target_account: target_account)
|
||||||
|
unless existing_follow.nil?
|
||||||
|
existing_follow.update!(uri: @json['id'])
|
||||||
AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
|
AuthorizeFollowService.new.call(@account, target_account, skip_follow_request: true, follow_request_uri: @json['id'])
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
@ -88,10 +88,18 @@ class Webfinger
|
||||||
end
|
end
|
||||||
|
|
||||||
def standard_url
|
def standard_url
|
||||||
"https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
|
if @domain.ends_with? ".onion"
|
||||||
|
"http://#{@domain}/.well-known/webfinger?resource=#{@uri}"
|
||||||
|
else
|
||||||
|
"https://#{@domain}/.well-known/webfinger?resource=#{@uri}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def host_meta_url
|
def host_meta_url
|
||||||
"https://#{@domain}/.well-known/host-meta"
|
if @domain.ends_with? ".onion"
|
||||||
|
"http://#{@domain}/.well-known/host-meta"
|
||||||
|
else
|
||||||
|
"https://#{@domain}/.well-known/host-meta"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,8 @@ module AccountFinderConcern
|
||||||
|
|
||||||
def representative
|
def representative
|
||||||
Account.find(-99)
|
Account.find(-99)
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_local(username)
|
def find_local(username)
|
||||||
|
|
|
@ -59,7 +59,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
IMAGE_STYLES = {
|
IMAGE_STYLES = {
|
||||||
original: {
|
original: {
|
||||||
pixels: 1_638_400, # 1280x1280px
|
pixels: 2_073_600, # 1920x1080px
|
||||||
file_geometry_parser: FastGeometryParser,
|
file_geometry_parser: FastGeometryParser,
|
||||||
}.freeze,
|
}.freeze,
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,17 @@ class FetchOEmbedService
|
||||||
|
|
||||||
return if @endpoint_url.blank?
|
return if @endpoint_url.blank?
|
||||||
|
|
||||||
@endpoint_url = (Addressable::URI.parse(@url) + @endpoint_url).to_s
|
@endpoint_url = begin
|
||||||
|
base_url = Addressable::URI.parse(@url)
|
||||||
|
|
||||||
|
# If the OEmbed endpoint is given as http but the URL we opened
|
||||||
|
# was served over https, we can assume OEmbed will be available
|
||||||
|
# through https as well
|
||||||
|
|
||||||
|
(base_url + @endpoint_url).tap do |absolute_url|
|
||||||
|
absolute_url.scheme = base_url.scheme if base_url.scheme == 'https'
|
||||||
|
end.to_s
|
||||||
|
end
|
||||||
|
|
||||||
cache_endpoint!
|
cache_endpoint!
|
||||||
rescue Addressable::URI::InvalidURIError
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
|
|
@ -11,5 +11,6 @@
|
||||||
.container-alt= yield
|
.container-alt= yield
|
||||||
.modal-layout__mastodon
|
.modal-layout__mastodon
|
||||||
%div
|
%div
|
||||||
|
%img{alt:'', draggable:'false', src:"#{mascot_url}"}
|
||||||
|
|
||||||
= render template: 'layouts/application'
|
= render template: 'layouts/application'
|
||||||
|
|
|
@ -25,6 +25,8 @@ require_relative '../lib/devise/two_factor_pam_authenticatable'
|
||||||
require_relative '../lib/chewy/strategy/custom_sidekiq'
|
require_relative '../lib/chewy/strategy/custom_sidekiq'
|
||||||
require_relative '../lib/webpacker/manifest_extensions'
|
require_relative '../lib/webpacker/manifest_extensions'
|
||||||
require_relative '../lib/webpacker/helper_extensions'
|
require_relative '../lib/webpacker/helper_extensions'
|
||||||
|
require_relative '../lib/action_dispatch/cookie_jar_extensions'
|
||||||
|
require_relative '../lib/rails/engine_extensions'
|
||||||
|
|
||||||
Dotenv::Railtie.load
|
Dotenv::Railtie.load
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActionDispatch
|
||||||
|
module CookieJarExtensions
|
||||||
|
private
|
||||||
|
|
||||||
|
# Monkey-patch ActionDispatch to serve secure cookies to Tor Hidden Service
|
||||||
|
# users. Otherwise, ActionDispatch would drop the cookie over HTTP.
|
||||||
|
def write_cookie?(*)
|
||||||
|
request.headers['Host'].ends_with?('.onion') || super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActionDispatch::Cookies::CookieJar.prepend(ActionDispatch::CookieJarExtensions)
|
|
@ -0,0 +1,11 @@
|
||||||
|
module Rails
|
||||||
|
module EngineExtensions
|
||||||
|
# Rewrite task loading code to filter digitalocean.rake task
|
||||||
|
def run_tasks_blocks(app)
|
||||||
|
Railtie.instance_method(:run_tasks_blocks).bind(self).call(app)
|
||||||
|
paths["lib/tasks"].existent.reject { |ext| ext.end_with?('digitalocean.rake') }.sort.each { |ext| load(ext) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails::Engine.prepend(Rails::EngineExtensions)
|
30
package.json
30
package.json
|
@ -60,18 +60,18 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.16",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/plugin-proposal-decorators": "^7.12.12",
|
"@babel/plugin-proposal-decorators": "^7.12.13",
|
||||||
"@babel/plugin-transform-react-inline-elements": "^7.12.1",
|
"@babel/plugin-transform-react-inline-elements": "^7.12.13",
|
||||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
"@babel/plugin-transform-runtime": "^7.12.15",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.16",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.13",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.13",
|
||||||
"@clusterws/cws": "^3.0.0",
|
"@clusterws/cws": "^3.0.0",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^0.5.7",
|
"@github/webauthn-json": "^0.5.7",
|
||||||
"@rails/ujs": "^6.1.1",
|
"@rails/ujs": "^6.1.2",
|
||||||
"array-includes": "^3.1.2",
|
"array-includes": "^3.1.2",
|
||||||
"atrament": "0.2.4",
|
"atrament": "0.2.4",
|
||||||
"arrow-key-navigation": "^1.2.0",
|
"arrow-key-navigation": "^1.2.0",
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
"color-blend": "^3.0.1",
|
"color-blend": "^3.0.1",
|
||||||
"compression-webpack-plugin": "^6.1.1",
|
"compression-webpack-plugin": "^6.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^5.0.1",
|
"css-loader": "^5.0.2",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"detect-passive-events": "^2.0.2",
|
"detect-passive-events": "^2.0.2",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
@ -114,7 +114,7 @@
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"mark-loader": "^0.1.6",
|
"mark-loader": "^0.1.6",
|
||||||
"marky": "^1.2.1",
|
"marky": "^1.2.1",
|
||||||
"mini-css-extract-plugin": "^1.3.5",
|
"mini-css-extract-plugin": "^1.3.6",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"npmlog": "^4.1.2",
|
"npmlog": "^4.1.2",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
"react-select": "^3.2.0",
|
"react-select": "^3.2.0",
|
||||||
"react-sparklines": "^1.7.0",
|
"react-sparklines": "^1.7.0",
|
||||||
"react-swipeable-views": "^0.13.9",
|
"react-swipeable-views": "^0.13.9",
|
||||||
"react-textarea-autosize": "^8.3.0",
|
"react-textarea-autosize": "^8.3.1",
|
||||||
"react-toggle": "^4.1.1",
|
"react-toggle": "^4.1.1",
|
||||||
"redis": "^3.0.2",
|
"redis": "^3.0.2",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
"requestidlecallback": "^0.3.0",
|
"requestidlecallback": "^0.3.0",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "^1.32.5",
|
"sass": "^1.32.7",
|
||||||
"sass-loader": "^10.1.1",
|
"sass-loader": "^10.1.1",
|
||||||
"stacktrace-js": "^2.0.2",
|
"stacktrace-js": "^2.0.2",
|
||||||
"stringz": "^2.1.0",
|
"stringz": "^2.1.0",
|
||||||
|
@ -175,13 +175,13 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
"@testing-library/react": "^11.2.3",
|
"@testing-library/react": "^11.2.5",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"eslint": "^7.18.0",
|
"eslint": "^7.19.0",
|
||||||
"eslint-plugin-import": "~2.22.1",
|
"eslint-plugin-import": "~2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "~6.4.1",
|
"eslint-plugin-jsx-a11y": "~6.4.1",
|
||||||
"eslint-plugin-promise": "~4.2.1",
|
"eslint-plugin-promise": "~4.3.1",
|
||||||
"eslint-plugin-react": "~7.22.0",
|
"eslint-plugin-react": "~7.22.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
|
|
|
@ -17,62 +17,171 @@ RSpec.describe ActivityPub::Activity::Follow do
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
subject { described_class.new(json, sender) }
|
subject { described_class.new(json, sender) }
|
||||||
|
|
||||||
context 'unlocked account' do
|
context 'with no prior follow' do
|
||||||
before do
|
context 'unlocked account' do
|
||||||
subject.perform
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a follow from sender to recipient' do
|
||||||
|
expect(sender.following?(recipient)).to be true
|
||||||
|
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(sender.requested?(recipient)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow from sender to recipient' do
|
context 'silenced account following an unlocked account' do
|
||||||
expect(sender.following?(recipient)).to be true
|
before do
|
||||||
|
sender.touch(:silenced_at)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow from sender to recipient' do
|
||||||
|
expect(sender.following?(recipient)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a follow request' do
|
||||||
|
expect(sender.requested?(recipient)).to be true
|
||||||
|
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create a follow request' do
|
context 'unlocked account muting the sender' do
|
||||||
expect(sender.requested?(recipient)).to be false
|
before do
|
||||||
|
recipient.mute!(sender)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a follow from sender to recipient' do
|
||||||
|
expect(sender.following?(recipient)).to be true
|
||||||
|
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(sender.requested?(recipient)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'locked account' do
|
||||||
|
before do
|
||||||
|
recipient.update(locked: true)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow from sender to recipient' do
|
||||||
|
expect(sender.following?(recipient)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a follow request' do
|
||||||
|
expect(sender.requested?(recipient)).to be true
|
||||||
|
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'silenced account following an unlocked account' do
|
context 'when a follow relationship already exists' do
|
||||||
before do
|
before do
|
||||||
sender.touch(:silenced_at)
|
sender.active_relationships.create!(target_account: recipient, uri: 'bar')
|
||||||
subject.perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create a follow from sender to recipient' do
|
context 'unlocked account' do
|
||||||
expect(sender.following?(recipient)).to be false
|
before do
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'correctly sets the new URI' do
|
||||||
|
expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow request' do
|
||||||
|
expect(sender.requested?(recipient)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow request' do
|
context 'silenced account following an unlocked account' do
|
||||||
expect(sender.requested?(recipient)).to be true
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'unlocked account muting the sender' do
|
context 'when a follow request already exists' do
|
||||||
before do
|
before do
|
||||||
recipient.mute!(sender)
|
sender.follow_requests.create!(target_account: recipient, uri: 'bar')
|
||||||
subject.perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates a follow from sender to recipient' do
|
context 'silenced account following an unlocked account' do
|
||||||
expect(sender.following?(recipient)).to be true
|
before do
|
||||||
|
sender.touch(:silenced_at)
|
||||||
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a follow from sender to recipient' do
|
||||||
|
expect(sender.following?(recipient)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'correctly sets the new URI' do
|
||||||
|
expect(sender.requested?(recipient)).to be true
|
||||||
|
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create a follow request' do
|
context 'locked account' do
|
||||||
expect(sender.requested?(recipient)).to be false
|
before do
|
||||||
end
|
recipient.update(locked: true)
|
||||||
end
|
subject.perform
|
||||||
|
end
|
||||||
|
|
||||||
context 'locked account' do
|
it 'does not create a follow from sender to recipient' do
|
||||||
before do
|
expect(sender.following?(recipient)).to be false
|
||||||
recipient.update(locked: true)
|
end
|
||||||
subject.perform
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create a follow from sender to recipient' do
|
it 'correctly sets the new URI' do
|
||||||
expect(sender.following?(recipient)).to be false
|
expect(sender.requested?(recipient)).to be true
|
||||||
end
|
expect(sender.follow_requests.find_by(target_account: recipient).uri).to eq 'foo'
|
||||||
|
end
|
||||||
it 'creates a follow request' do
|
|
||||||
expect(sender.requested?(recipient)).to be true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue