Merge pull request #3346 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 19bc3e76ea1d7fa2d9af711613a77fc0f5f0d1b5
This commit is contained in:
commit
46df08b3a4
18
Gemfile.lock
18
Gemfile.lock
@ -96,8 +96,8 @@ GEM
|
|||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1200.0)
|
aws-partitions (1.1201.0)
|
||||||
aws-sdk-core (3.240.0)
|
aws-sdk-core (3.241.3)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
@ -105,11 +105,11 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.118.0)
|
aws-sdk-kms (1.120.0)
|
||||||
aws-sdk-core (~> 3, >= 3.239.1)
|
aws-sdk-core (~> 3, >= 3.241.3)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.209.0)
|
aws-sdk-s3 (1.211.0)
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
aws-sdk-core (~> 3, >= 3.241.3)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.12.1)
|
||||||
@ -752,7 +752,7 @@ GEM
|
|||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 9)
|
sidekiq (>= 5, < 9)
|
||||||
rspec-support (3.13.6)
|
rspec-support (3.13.6)
|
||||||
rubocop (1.81.7)
|
rubocop (1.82.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@ -760,7 +760,7 @@ GEM
|
|||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rubocop-ast (>= 1.47.1, < 2.0)
|
rubocop-ast (>= 1.48.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.49.0)
|
rubocop-ast (1.49.0)
|
||||||
@ -782,7 +782,7 @@ GEM
|
|||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.75.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.44.0, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rspec (3.8.0)
|
rubocop-rspec (3.9.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.81)
|
rubocop (~> 1.81)
|
||||||
rubocop-rspec_rails (2.32.0)
|
rubocop-rspec_rails (2.32.0)
|
||||||
|
|||||||
@ -1,9 +1,24 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ThemeHelper
|
module ThemeHelper
|
||||||
|
def javascript_inline_tag(path)
|
||||||
|
entry = InlineScriptManager.instance.file(path)
|
||||||
|
|
||||||
|
# Only add hash if we don't allow arbitrary includes already, otherwise it's going
|
||||||
|
# to break the React Tools browser extension or other inline scripts
|
||||||
|
unless Rails.env.development? && request.content_security_policy.dup.script_src.include?("'unsafe-inline'")
|
||||||
|
request.content_security_policy = request.content_security_policy.clone.tap do |policy|
|
||||||
|
values = policy.script_src
|
||||||
|
values << "'sha256-#{entry[:digest]}'"
|
||||||
|
policy.script_src(*values)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
content_tag(:script, entry[:contents], type: 'text/javascript')
|
||||||
|
end
|
||||||
|
|
||||||
def theme_style_tags(flavour_and_skin)
|
def theme_style_tags(flavour_and_skin)
|
||||||
flavour, theme = flavour_and_skin
|
flavour, theme = flavour_and_skin
|
||||||
|
|
||||||
if theme == 'system'
|
if theme == 'system'
|
||||||
''.html_safe.tap do |tags|
|
''.html_safe.tap do |tags|
|
||||||
tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light", type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light", type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
|
|||||||
setIndex(newIndex);
|
setIndex(newIndex);
|
||||||
setZoomedIn(false);
|
setZoomedIn(false);
|
||||||
if (animate) {
|
if (animate) {
|
||||||
void api.start({ x: `-${newIndex * 100}%` });
|
void api.start({ x: `calc(-${newIndex * 100}% + 0px)` });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[api, media.size],
|
[api, media.size],
|
||||||
|
|||||||
@ -12,8 +12,21 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features = 'fasp' | 'http_message_signatures';
|
export type ServerFeatures = 'fasp';
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isServerFeatureEnabled(feature: ServerFeatures) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientFeatures = 'profile_redesign';
|
||||||
|
|
||||||
|
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
||||||
|
try {
|
||||||
|
const features =
|
||||||
|
window.localStorage.getItem('experiments')?.split(',') ?? [];
|
||||||
|
return features.includes(feature);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not access localStorage to get client features', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,9 +5,7 @@ export function getUserTheme() {
|
|||||||
|
|
||||||
export function isDarkMode() {
|
export function isDarkMode() {
|
||||||
const { userTheme } = document.documentElement.dataset;
|
const { userTheme } = document.documentElement.dataset;
|
||||||
return (
|
return userTheme === 'system'
|
||||||
(userTheme === 'system' &&
|
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches) ||
|
: userTheme !== 'mastodon-light';
|
||||||
userTheme !== 'mastodon-light'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/javascript/inline/theme-selection.js
Normal file
23
app/javascript/inline/theme-selection.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
(function (element) {
|
||||||
|
const {userTheme} = element.dataset;
|
||||||
|
|
||||||
|
const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)');
|
||||||
|
|
||||||
|
const updateColorScheme = () => {
|
||||||
|
const useDarkMode = userTheme === 'system' ? colorSchemeMediaWatcher.matches : userTheme !== 'mastodon-light';
|
||||||
|
element.dataset.mode = useDarkMode ? 'dark' : 'light';
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContrast = () => {
|
||||||
|
const useHighContrast = userTheme === 'contrast' || contrastMediaWatcher.matches;
|
||||||
|
|
||||||
|
element.dataset.contrast = useHighContrast ? 'high' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
colorSchemeMediaWatcher.addEventListener('change', updateColorScheme);
|
||||||
|
contrastMediaWatcher.addEventListener('change', updateContrast);
|
||||||
|
|
||||||
|
updateColorScheme();
|
||||||
|
updateContrast();
|
||||||
|
})(document.documentElement);
|
||||||
@ -85,7 +85,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
|
|||||||
setIndex(newIndex);
|
setIndex(newIndex);
|
||||||
setZoomedIn(false);
|
setZoomedIn(false);
|
||||||
if (animate) {
|
if (animate) {
|
||||||
void api.start({ x: `-${newIndex * 100}%` });
|
void api.start({ x: `calc(-${newIndex * 100}% + 0px)` });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[api, media.size],
|
[api, media.size],
|
||||||
|
|||||||
@ -12,8 +12,21 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features = 'fasp' | 'http_message_signatures';
|
export type ServerFeatures = 'fasp';
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isServerFeatureEnabled(feature: ServerFeatures) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientFeatures = 'profile_redesign';
|
||||||
|
|
||||||
|
export function isClientFeatureEnabled(feature: ClientFeatures) {
|
||||||
|
try {
|
||||||
|
const features =
|
||||||
|
window.localStorage.getItem('experiments')?.split(',') ?? [];
|
||||||
|
return features.includes(feature);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not access localStorage to get client features', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -5,9 +5,7 @@ export function getUserTheme() {
|
|||||||
|
|
||||||
export function isDarkMode() {
|
export function isDarkMode() {
|
||||||
const { userTheme } = document.documentElement.dataset;
|
const { userTheme } = document.documentElement.dataset;
|
||||||
return (
|
return userTheme === 'system'
|
||||||
(userTheme === 'system' &&
|
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches) ||
|
: userTheme !== 'mastodon-light';
|
||||||
userTheme !== 'mastodon-light'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
app/lib/inline_script_manager.rb
Normal file
31
app/lib/inline_script_manager.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'singleton'
|
||||||
|
|
||||||
|
class InlineScriptManager
|
||||||
|
include Singleton
|
||||||
|
include ActionView::Helpers::TagHelper
|
||||||
|
include ActionView::Helpers::JavaScriptHelper
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@cached_files = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def file(name)
|
||||||
|
@cached_files[name] ||= load_file(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_file(name)
|
||||||
|
path = Pathname.new(name).cleanpath
|
||||||
|
raise ArgumentError, "Invalid inline javascript path: #{path}" if path.to_s.start_with?('..')
|
||||||
|
|
||||||
|
path = Rails.root.join('app', 'javascript', 'inline', path)
|
||||||
|
|
||||||
|
contents = javascript_cdata_section(path.read)
|
||||||
|
digest = Digest::SHA256.base64digest(contents)
|
||||||
|
|
||||||
|
{ contents:, digest: }
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -58,13 +58,7 @@ class FetchResourceService < BaseService
|
|||||||
|
|
||||||
[@url, { prefetched_body: body }]
|
[@url, { prefetched_body: body }]
|
||||||
elsif !terminal
|
elsif !terminal
|
||||||
link_header = response['Link'] && parse_link_header(response)
|
process_link_headers(response) || process_html(response)
|
||||||
|
|
||||||
if link_header&.find_link(%w(rel alternate))
|
|
||||||
process_link_headers(link_header)
|
|
||||||
elsif response.mime_type == 'text/html'
|
|
||||||
process_html(response)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -73,13 +67,18 @@ class FetchResourceService < BaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_html(response)
|
def process_html(response)
|
||||||
|
return unless response.mime_type == 'text/html'
|
||||||
|
|
||||||
page = Nokogiri::HTML5(response.body_with_limit)
|
page = Nokogiri::HTML5(response.body_with_limit)
|
||||||
json_link = page.xpath('//link[nokogiri:link_rel_include(@rel, "alternate")]', NokogiriHandler).find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) }
|
json_link = page.xpath('//link[nokogiri:link_rel_include(@rel, "alternate")]', NokogiriHandler).find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) }
|
||||||
|
|
||||||
process(json_link['href'], terminal: true) unless json_link.nil?
|
process(json_link['href'], terminal: true) unless json_link.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_link_headers(link_header)
|
def process_link_headers(response)
|
||||||
|
link_header = response['Link'] && parse_link_header(response)
|
||||||
|
return if link_header.nil?
|
||||||
|
|
||||||
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
|
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
|
||||||
|
|
||||||
process(json_link.href, terminal: true) unless json_link.nil?
|
process(json_link.href, terminal: true) unless json_link.nil?
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
- if use_mask_icon?
|
- if use_mask_icon?
|
||||||
%link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/
|
%link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/
|
||||||
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
|
||||||
|
= javascript_inline_tag 'theme-selection.js'
|
||||||
= theme_color_tags current_theme
|
= theme_color_tags current_theme
|
||||||
%meta{ name: 'mobile-web-app-capable', content: 'yes' }/
|
%meta{ name: 'mobile-web-app-capable', content: 'yes' }/
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class ActivityPub::DeliveryWorker
|
|||||||
if @inbox_url.present?
|
if @inbox_url.present?
|
||||||
if @performed
|
if @performed
|
||||||
failure_tracker.track_success!
|
failure_tracker.track_success!
|
||||||
else
|
elsif !@unsalvageable
|
||||||
failure_tracker.track_failure!
|
failure_tracker.track_failure!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -62,9 +62,13 @@ class ActivityPub::DeliveryWorker
|
|||||||
stoplight_wrapper.run do
|
stoplight_wrapper.run do
|
||||||
request_pool.with(@host) do |http_client|
|
request_pool.with(@host) do |http_client|
|
||||||
build_request(http_client).perform do |response|
|
build_request(http_client).perform do |response|
|
||||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response)
|
if response_successful?(response)
|
||||||
|
@performed = true
|
||||||
@performed = true
|
elsif response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response)
|
||||||
|
@unsalvageable = true
|
||||||
|
else
|
||||||
|
raise Mastodon::UnexpectedResponseError, response
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -45,30 +45,34 @@ class MoveWorker
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Then handle accounts that follow both the old and new account
|
# Then handle accounts that follow both the old and new account
|
||||||
@source_account.passive_relationships
|
source_local_followers
|
||||||
.where(account: Account.local)
|
.where(account: @target_account.followers.local)
|
||||||
.where(account: @target_account.followers.local)
|
.in_batches do |follows|
|
||||||
.in_batches do |follows|
|
ListAccount.where(follow: follows).includes(:list).find_each do |list_account|
|
||||||
ListAccount.where(follow: follows).includes(:list).find_each do |list_account|
|
list_account.list.accounts << @target_account
|
||||||
list_account.list.accounts << @target_account
|
rescue ActiveRecord::RecordInvalid
|
||||||
rescue ActiveRecord::RecordInvalid
|
nil
|
||||||
nil
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Finally, handle the common case of accounts not following the new account
|
# Finally, handle the common case of accounts not following the new account
|
||||||
@source_account.passive_relationships
|
source_local_followers
|
||||||
.where(account: Account.local)
|
.where.not(account: @target_account.followers.local)
|
||||||
.where.not(account: @target_account.followers.local)
|
.where.not(account_id: @target_account.id)
|
||||||
.where.not(account_id: @target_account.id)
|
.in_batches do |follows|
|
||||||
.in_batches do |follows|
|
ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
|
||||||
ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id)
|
num_moved += follows.update_all(target_account_id: @target_account.id)
|
||||||
num_moved += follows.update_all(target_account_id: @target_account.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
num_moved
|
num_moved
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def source_local_followers
|
||||||
|
@source_account
|
||||||
|
.passive_relationships
|
||||||
|
.where(account: Account.local)
|
||||||
|
end
|
||||||
|
|
||||||
def queue_follow_unfollows!
|
def queue_follow_unfollows!
|
||||||
bypass_locked = @target_account.local?
|
bypass_locked = @target_account.local?
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,7 @@ module Paperclip
|
|||||||
# if we're processing the original, close + unlink the source tempfile
|
# if we're processing the original, close + unlink the source tempfile
|
||||||
intermediate_files << original if name == :original
|
intermediate_files << original if name == :original
|
||||||
|
|
||||||
@queued_for_write[name] = style.processors
|
@queued_for_write[name] = style.processors.inject(original) do |file, processor|
|
||||||
.inject(original) do |file, processor|
|
|
||||||
file = Paperclip.processor(processor).make(file, style.processor_options, self)
|
file = Paperclip.processor(processor).make(file, style.processor_options, self)
|
||||||
intermediate_files << file unless file == original
|
intermediate_files << file unless file == original
|
||||||
file
|
file
|
||||||
|
|||||||
@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
.to start_with('application/json')
|
.to start_with('application/json')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when status has non-default quote policy and param is omitted' do
|
||||||
|
let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') }
|
||||||
|
|
||||||
|
it 'preserves existing quote approval policy' do
|
||||||
|
expect { subject }
|
||||||
|
.to_not(change { status.reload.quote_approval_policy })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do
|
|||||||
img-src 'self' data: blob: #{local_domain}
|
img-src 'self' data: blob: #{local_domain}
|
||||||
manifest-src 'self' #{local_domain}
|
manifest-src 'self' #{local_domain}
|
||||||
media-src 'self' data: #{local_domain}
|
media-src 'self' data: #{local_domain}
|
||||||
script-src 'self' #{local_domain} 'wasm-unsafe-eval'
|
script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Q/2Cjx8v06hAdOF8/DeBUpsmBcSj7sLN3I/WpTF8T8c='
|
||||||
style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ=='
|
style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ=='
|
||||||
worker-src 'self' blob: #{local_domain}
|
worker-src 'self' blob: #{local_domain}
|
||||||
CSP
|
CSP
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user