Merge branch 'main' into mute-prefill

This commit is contained in:
Sebastian Hädrich 2025-06-25 23:01:35 +02:00 committed by GitHub
commit cce87b7f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 64 additions and 78 deletions

2
.nvmrc
View File

@ -1 +1 @@
22.16
22.17

View File

@ -6,6 +6,8 @@ class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :store_current_location
before_action :authenticate_resource_owner!
layout 'modal'
content_security_policy do |p|
p.form_action(false)
end

View File

@ -11,6 +11,8 @@ class OAuth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
skip_before_action :require_functional!
layout 'admin'
include Localized
def destroy

View File

@ -18,6 +18,7 @@ import initialState, { title as siteTitle } from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales';
import { store } from 'mastodon/store';
import { isProduction } from 'mastodon/utils/environment';
import { BodyScrollLock } from 'mastodon/features/ui/components/body_scroll_lock';
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
@ -58,6 +59,7 @@ export default class Mastodon extends PureComponent {
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
<Route path='/' component={UI} />
</ScrollContext>
<BodyScrollLock />
</Router>
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />

View File

@ -14,7 +14,6 @@ import MediaModal from 'mastodon/features/ui/components/media_modal';
import { Video } from 'mastodon/features/video';
import { IntlProvider } from 'mastodon/locales';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
@ -34,9 +33,6 @@ export default class MediaContainer extends PureComponent {
};
handleOpenMedia = (media, index, lang) => {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media, index, lang });
};
@ -45,16 +41,10 @@ export default class MediaContainer extends PureComponent {
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
const mediaList = fromJS(media);
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
this.setState({ media: mediaList, lang, options });
};
handleCloseMedia = () => {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = '0';
this.setState({
media: null,
index: null,

View File

@ -0,0 +1,30 @@
import { useLayoutEffect } from 'react';
import { createAppSelector, useAppSelector } from 'mastodon/store';
const getShouldLockBodyScroll = createAppSelector(
[
(state) => state.navigation.open,
(state) => state.modal.get('stack').size > 0,
],
(isMobileMenuOpen: boolean, isModalOpen: boolean) =>
isMobileMenuOpen || isModalOpen,
);
/**
* This component locks scrolling on the body when
* `getShouldLockBodyScroll` returns true.
*/
export const BodyScrollLock: React.FC = () => {
const shouldLockBodyScroll = useAppSelector(getShouldLockBodyScroll);
useLayoutEffect(() => {
document.documentElement.classList.toggle(
'has-modal',
shouldLockBodyScroll,
);
}, [shouldLockBodyScroll]);
return null;
};

View File

@ -20,7 +20,6 @@ import {
IgnoreNotificationsModal,
AnnualReportModal,
} from 'mastodon/features/ui/util/async-components';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
import BundleContainer from '../containers/bundle_container';
@ -90,20 +89,6 @@ export default class ModalRoot extends PureComponent {
backgroundColor: null,
};
getSnapshotBeforeUpdate () {
return { visible: !!this.props.type };
}
componentDidUpdate (prevProps, prevState, { visible }) {
if (visible) {
document.body.classList.add('with-modals--active');
document.documentElement.style.marginRight = `${getScrollbarWidth()}px`;
} else {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = '0';
}
}
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
};

View File

@ -1,31 +0,0 @@
import { isMobile } from '../is_mobile';
let cachedScrollbarWidth: number | null = null;
const getActualScrollbarWidth = () => {
const outer = document.createElement('div');
outer.style.visibility = 'hidden';
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
const inner = document.createElement('div');
outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
outer.remove();
return scrollbarWidth;
};
export const getScrollbarWidth = () => {
if (cachedScrollbarWidth !== null) {
return cachedScrollbarWidth;
}
const scrollbarWidth = isMobile(window.innerWidth)
? 0
: getActualScrollbarWidth();
cachedScrollbarWidth = scrollbarWidth;
return scrollbarWidth;
};

View File

@ -1,6 +1,20 @@
@use 'variables' as *;
@use 'functions' as *;
html.has-modal {
&,
body {
touch-action: none;
overscroll-behavior: none;
-webkit-overflow-scrolling: auto;
scrollbar-gutter: stable;
}
body {
overflow: hidden !important;
}
}
body {
font-family: $font-sans-serif, sans-serif;
background: var(--background-color);
@ -64,20 +78,6 @@ body {
height: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
&.with-modals--active {
overflow-y: hidden;
overscroll-behavior: none;
}
}
&.with-modals {
overflow-x: hidden;
overflow-y: scroll;
&--active {
overflow-y: hidden;
}
}
&.player {

View File

@ -2894,6 +2894,7 @@ a.account__display-name {
background: var(--background-color);
backdrop-filter: var(--background-filter);
border-top: 1px solid var(--background-border-color);
box-sizing: border-box;
.layout-multiple-columns & {
display: none;
@ -3105,7 +3106,7 @@ a.account__display-name {
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px;
padding-block: 15px;
padding-inline-end: 30px;
}
@ -3165,7 +3166,7 @@ a.account__display-name {
.navigation-panel {
margin: 0;
border-inline-start: 1px solid var(--background-border-color);
height: 100vh;
height: 100dvh;
}
.navigation-panel__banner,
@ -3223,6 +3224,7 @@ a.account__display-name {
.navigation-panel {
width: 284px;
overflow-y: auto;
scrollbar-width: thin;
&__menu {
flex-shrink: 0;
@ -8972,7 +8974,7 @@ noscript {
.search__input {
border: 1px solid var(--background-border-color);
padding: 12px;
padding-block: 12px;
padding-inline-end: 30px;
}

View File

@ -19,7 +19,7 @@ class Rule < ApplicationRecord
self.discard_column = :deleted_at
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
has_many :translations, -> { order(language: :asc) }, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
accepts_nested_attributes_for :translations, reject_if: ->(attributes) { attributes['text'].blank? }, allow_destroy: true
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }

View File

@ -115,8 +115,6 @@ module Mastodon
end
config.to_prepare do
Doorkeeper::AuthorizationsController.layout 'modal'
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
Doorkeeper::Application.include ApplicationExtension
Doorkeeper::AccessGrant.include AccessGrantExtension
Doorkeeper::AccessToken.include AccessTokenExtension

View File

@ -9,7 +9,7 @@ module ViteRuby::ManifestIntegrityExtension
def load_manifest
# Invalidate the name lookup cache when reloading manifest
@name_lookup_cache = load_name_lookup_cache unless dev_server_running?
@name_lookup_cache = nil unless dev_server_running?
super
end

View File

@ -3,6 +3,8 @@
require 'rails_helper'
RSpec.describe OAuth::AuthorizationsController do
render_views
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
describe 'GET #new' do
@ -24,6 +26,8 @@ RSpec.describe OAuth::AuthorizationsController do
.to have_http_status(200)
expect(response.headers['Cache-Control'])
.to include('private, no-store')
expect(response.parsed_body.at('body.modal-layout'))
.to be_present
expect(controller.stored_location_for(:user))
.to eq authorize_path_for(app)
end

View File

@ -21,6 +21,8 @@ RSpec.describe OAuth::AuthorizedApplicationsController do
.to have_http_status(200)
expect(response.headers['Cache-Control'])
.to include('private, no-store')
expect(response.parsed_body.at('body.admin'))
.to be_present
expect(controller.stored_location_for(:user))
.to eq '/oauth/authorized_applications'
end