Merge branch 'main' into feature/require-mfa-by-admin

This commit is contained in:
FredysFonseca 2025-07-17 18:07:29 -04:00 committed by GitHub
commit 7b1f26eea2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 320 additions and 236 deletions

View File

@ -1 +1 @@
3.4.4 3.4.5

View File

@ -1,3 +1,5 @@
import { resolve } from 'node:path';
import type { StorybookConfig } from '@storybook/react-vite'; import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = { const config: StorybookConfig = {
@ -26,6 +28,12 @@ const config: StorybookConfig = {
'oops.png', 'oops.png',
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })), ].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
], ],
viteFinal(config) {
// For an unknown reason, Storybook does not use the root
// from the Vite config so we need to set it manually.
config.root = resolve(__dirname, '../app/javascript');
return config;
},
}; };
export default config; export default config;

View File

@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.4" ARG RUBY_VERSION="3.4.5"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node # renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22" ARG NODE_MAJOR_VERSION="22"

View File

@ -224,7 +224,7 @@ GEM
mail (~> 2.7) mail (~> 2.7)
email_validator (2.2.4) email_validator (2.2.4)
activemodel activemodel
erb (5.0.1) erb (5.0.2)
erubi (1.13.1) erubi (1.13.1)
et-orbi (1.2.11) et-orbi (1.2.11)
tzinfo tzinfo
@ -315,7 +315,7 @@ GEM
http_accept_language (2.1.1) http_accept_language (2.1.1)
httpclient (2.9.0) httpclient (2.9.0)
mutex_m mutex_m
httplog (1.7.0) httplog (1.7.1)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.14.7) i18n (1.14.7)
@ -335,7 +335,7 @@ GEM
inline_svg (1.10.0) inline_svg (1.10.0)
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.8.0) io-console (0.8.1)
irb (1.15.2) irb (1.15.2)
pp (>= 0.6.0) pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
@ -627,11 +627,10 @@ GEM
prism (1.4.0) prism (1.4.0)
prometheus_exporter (2.2.0) prometheus_exporter (2.2.0)
webrick webrick
propshaft (1.1.0) propshaft (1.2.0)
actionpack (>= 7.0.0) actionpack (>= 7.0.0)
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
railties (>= 7.0.0)
psych (5.2.6) psych (5.2.6)
date date
stringio stringio
@ -708,7 +707,7 @@ GEM
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0) rdf-normalize (0.7.0)
rdf (~> 3.3) rdf (~> 3.3)
rdoc (6.14.1) rdoc (6.14.2)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.1) redcarpet (3.6.1)

View File

@ -261,7 +261,9 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
); );
const lang = useAppSelector( const lang = useAppSelector(
(state) => (state) =>
(state.compose as ImmutableMap<string, unknown>).get('lang') as string, (state.compose as ImmutableMap<string, unknown>).get(
'language',
) as string,
); );
const focusX = const focusX =
(media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0; (media?.getIn(['meta', 'focus', 'x'], 0) as number | undefined) ?? 0;

View File

@ -431,6 +431,7 @@ export const CollapsibleNavigationPanel: React.FC = () => {
filterTaps: true, filterTaps: true,
bounds: isLtrDir ? { left: 0 } : { right: 0 }, bounds: isLtrDir ? { left: 0 } : { right: 0 },
rubberband: true, rubberband: true,
enabled: openable,
}, },
); );

View File

@ -122,98 +122,93 @@ export const PolicyControls: React.FC = () => {
value={notificationPolicy.for_not_following} value={notificationPolicy.for_not_following}
onChange={handleFilterNotFollowing} onChange={handleFilterNotFollowing}
options={options} options={options}
> label={
<strong>
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_not_following_title' id='notifications.policy.filter_not_following_title'
defaultMessage="People you don't follow" defaultMessage="People you don't follow"
/> />
</strong> }
<span className='hint'> hint={
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_not_following_hint' id='notifications.policy.filter_not_following_hint'
defaultMessage='Until you manually approve them' defaultMessage='Until you manually approve them'
/> />
</span> }
</SelectWithLabel> />
<SelectWithLabel <SelectWithLabel
value={notificationPolicy.for_not_followers} value={notificationPolicy.for_not_followers}
onChange={handleFilterNotFollowers} onChange={handleFilterNotFollowers}
options={options} options={options}
> label={
<strong>
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_not_followers_title' id='notifications.policy.filter_not_followers_title'
defaultMessage='People not following you' defaultMessage='People not following you'
/> />
</strong> }
<span className='hint'> hint={
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_not_followers_hint' id='notifications.policy.filter_not_followers_hint'
defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}'
values={{ days: 3 }} values={{ days: 3 }}
/> />
</span> }
</SelectWithLabel> />
<SelectWithLabel <SelectWithLabel
value={notificationPolicy.for_new_accounts} value={notificationPolicy.for_new_accounts}
onChange={handleFilterNewAccounts} onChange={handleFilterNewAccounts}
options={options} options={options}
> label={
<strong>
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_new_accounts_title' id='notifications.policy.filter_new_accounts_title'
defaultMessage='New accounts' defaultMessage='New accounts'
/> />
</strong> }
<span className='hint'> hint={
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_new_accounts.hint' id='notifications.policy.filter_new_accounts.hint'
defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}'
values={{ days: 30 }} values={{ days: 30 }}
/> />
</span> }
</SelectWithLabel> />
<SelectWithLabel <SelectWithLabel
value={notificationPolicy.for_private_mentions} value={notificationPolicy.for_private_mentions}
onChange={handleFilterPrivateMentions} onChange={handleFilterPrivateMentions}
options={options} options={options}
> label={
<strong>
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_private_mentions_title' id='notifications.policy.filter_private_mentions_title'
defaultMessage='Unsolicited private mentions' defaultMessage='Unsolicited private mentions'
/> />
</strong> }
<span className='hint'> hint={
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_private_mentions_hint' id='notifications.policy.filter_private_mentions_hint'
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
/> />
</span> }
</SelectWithLabel> />
<SelectWithLabel <SelectWithLabel
value={notificationPolicy.for_limited_accounts} value={notificationPolicy.for_limited_accounts}
onChange={handleFilterLimitedAccounts} onChange={handleFilterLimitedAccounts}
options={options} options={options}
> label={
<strong>
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_limited_accounts_title' id='notifications.policy.filter_limited_accounts_title'
defaultMessage='Moderated accounts' defaultMessage='Moderated accounts'
/> />
</strong> }
<span className='hint'> hint={
<FormattedMessage <FormattedMessage
id='notifications.policy.filter_limited_accounts_hint' id='notifications.policy.filter_limited_accounts_hint'
defaultMessage='Limited by server moderators' defaultMessage='Limited by server moderators'
/> />
</span> }
</SelectWithLabel> />
</div> </div>
</section> </section>
); );

View File

@ -1,5 +1,5 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useCallback, useState, useRef } from 'react'; import { useCallback, useState, useRef, useId } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -16,6 +16,8 @@ interface DropdownProps {
options: SelectItem[]; options: SelectItem[];
disabled?: boolean; disabled?: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
'aria-labelledby': string;
'aria-describedby'?: string;
placement?: Placement; placement?: Placement;
} }
@ -24,51 +26,33 @@ const Dropdown: React.FC<DropdownProps> = ({
options, options,
disabled, disabled,
onChange, onChange,
'aria-labelledby': ariaLabelledBy,
'aria-describedby': ariaDescribedBy,
placement: initialPlacement = 'bottom-end', placement: initialPlacement = 'bottom-end',
}) => { }) => {
const activeElementRef = useRef<Element | null>(null); const containerRef = useRef<HTMLDivElement>(null);
const containerRef = useRef(null); const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setOpen] = useState<boolean>(false); const [isOpen, setOpen] = useState<boolean>(false);
const [placement, setPlacement] = useState<Placement>(initialPlacement); const [placement, setPlacement] = useState<Placement>(initialPlacement);
const uniqueId = useId();
const handleToggle = useCallback(() => { const menuId = `${uniqueId}-menu`;
if ( const buttonLabelId = `${uniqueId}-button`;
isOpen &&
activeElementRef.current &&
activeElementRef.current instanceof HTMLElement
) {
activeElementRef.current.focus({ preventScroll: true });
}
setOpen(!isOpen);
}, [isOpen, setOpen]);
const handleMouseDown = useCallback(() => {
if (!isOpen) activeElementRef.current = document.activeElement;
}, [isOpen]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
switch (e.key) {
case ' ':
case 'Enter':
if (!isOpen) activeElementRef.current = document.activeElement;
break;
}
},
[isOpen],
);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if ( if (isOpen && buttonRef.current) {
isOpen && buttonRef.current.focus({ preventScroll: true });
activeElementRef.current && }
activeElementRef.current instanceof HTMLElement
)
activeElementRef.current.focus({ preventScroll: true });
setOpen(false); setOpen(false);
}, [isOpen]); }, [isOpen]);
const handleToggle = useCallback(() => {
if (isOpen) {
handleClose();
} else {
setOpen(true);
}
}, [isOpen, handleClose]);
const handleOverlayEnter = useCallback( const handleOverlayEnter = useCallback(
(state: Partial<PopperState>) => { (state: Partial<PopperState>) => {
if (state.placement) setPlacement(state.placement); if (state.placement) setPlacement(state.placement);
@ -82,13 +66,18 @@ const Dropdown: React.FC<DropdownProps> = ({
<div ref={containerRef}> <div ref={containerRef}>
<button <button
type='button' type='button'
ref={buttonRef}
onClick={handleToggle} onClick={handleToggle}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
disabled={disabled} disabled={disabled}
aria-expanded={isOpen}
aria-controls={menuId}
aria-labelledby={`${ariaLabelledBy} ${buttonLabelId}`}
aria-describedby={ariaDescribedBy}
className={classNames('dropdown-button', { active: isOpen })} className={classNames('dropdown-button', { active: isOpen })}
> >
<span className='dropdown-button__label'>{valueOption?.text}</span> <span id={buttonLabelId} className='dropdown-button__label'>
{valueOption?.text}
</span>
<Icon id='down' icon={ArrowDropDownIcon} /> <Icon id='down' icon={ArrowDropDownIcon} />
</button> </button>
@ -101,7 +90,7 @@ const Dropdown: React.FC<DropdownProps> = ({
popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }} popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
> >
{({ props, placement }) => ( {({ props, placement }) => (
<div {...props}> <div {...props} id={menuId}>
<div <div
className={`dropdown-animation privacy-dropdown__dropdown ${placement}`} className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
> >
@ -123,6 +112,8 @@ const Dropdown: React.FC<DropdownProps> = ({
interface Props { interface Props {
value: string; value: string;
options: SelectItem[]; options: SelectItem[];
label: string | React.ReactElement;
hint: string | React.ReactElement;
disabled?: boolean; disabled?: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
} }
@ -130,13 +121,26 @@ interface Props {
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({ export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
value, value,
options, options,
label,
hint,
disabled, disabled,
children,
onChange, onChange,
}) => { }) => {
const uniqueId = useId();
const labelId = `${uniqueId}-label`;
const descId = `${uniqueId}-desc`;
return ( return (
// This label is only used for its click-forwarding behaviour,
// accessible names are assigned manually
// eslint-disable-next-line jsx-a11y/label-has-associated-control
<label className='app-form__toggle'> <label className='app-form__toggle'>
<div className='app-form__toggle__label'>{children}</div> <div className='app-form__toggle__label'>
<strong id={labelId}>{label}</strong>
<span className='hint' id={descId}>
{hint}
</span>
</div>
<div className='app-form__toggle__toggle'> <div className='app-form__toggle__toggle'>
<div> <div>
@ -144,6 +148,8 @@ export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
value={value} value={value}
onChange={onChange} onChange={onChange}
disabled={disabled} disabled={disabled}
aria-labelledby={labelId}
aria-describedby={descId}
options={options} options={options}
/> />
</div> </div>

View File

@ -617,7 +617,7 @@
"notification.reblog": "{name} продвинул(а) ваш пост", "notification.reblog": "{name} продвинул(а) ваш пост",
"notification.reblog.name_and_others_with_link": "{name} и ещё <a>{count, plural, one {# пользователь} few {# пользователя} other {# пользователей}}</a> продвинули ваш пост", "notification.reblog.name_and_others_with_link": "{name} и ещё <a>{count, plural, one {# пользователь} few {# пользователя} other {# пользователей}}</a> продвинули ваш пост",
"notification.relationships_severance_event": "Разорвана связь с {name}", "notification.relationships_severance_event": "Разорвана связь с {name}",
"notification.relationships_severance_event.account_suspension": "Администратор сервера {from} заблокировал сервер {target}, поэтому вы больше не сможете получать обновления от людей с этого сервера или взаимодействовать с ними.", "notification.relationships_severance_event.account_suspension": "Администратор сервера {from} заблокировал сервер {target}, поэтому вы больше не сможете получать обновления от людей с этого сервера и взаимодействовать с ними.",
"notification.relationships_severance_event.domain_block": "Администратор сервера {from} заблокировал сервер {target}, где размещены учётные записи {followersCount} ваших подписчиков и {followingCount, plural, one {# пользователя, на которого вы подписаны} other {# пользователей, на которых вы подписаны}}.", "notification.relationships_severance_event.domain_block": "Администратор сервера {from} заблокировал сервер {target}, где размещены учётные записи {followersCount} ваших подписчиков и {followingCount, plural, one {# пользователя, на которого вы подписаны} other {# пользователей, на которых вы подписаны}}.",
"notification.relationships_severance_event.learn_more": "Узнать больше", "notification.relationships_severance_event.learn_more": "Узнать больше",
"notification.relationships_severance_event.user_domain_block": "Вы заблокировали сервер {target}, где размещены учётные записи {followersCount} ваших подписчиков и {followingCount, plural, one {# пользователя, на которого вы подписаны} other {# пользователей, на которых вы подписаны}}.", "notification.relationships_severance_event.user_domain_block": "Вы заблокировали сервер {target}, где размещены учётные записи {followersCount} ваших подписчиков и {followingCount, plural, one {# пользователя, на которого вы подписаны} other {# пользователей, на которых вы подписаны}}.",
@ -707,7 +707,7 @@
"onboarding.profile.display_name": "Отображаемое имя", "onboarding.profile.display_name": "Отображаемое имя",
"onboarding.profile.display_name_hint": "Ваше полное имя или псевдоним…", "onboarding.profile.display_name_hint": "Ваше полное имя или псевдоним…",
"onboarding.profile.note": "О себе", "onboarding.profile.note": "О себе",
"onboarding.profile.note_hint": "Вы можете @упоминать других людей или использовать #хештеги…", "onboarding.profile.note_hint": "Вы можете @упоминать других людей, а также использовать #хештеги…",
"onboarding.profile.save_and_continue": "Сохранить и продолжить", "onboarding.profile.save_and_continue": "Сохранить и продолжить",
"onboarding.profile.title": "Создайте свой профиль", "onboarding.profile.title": "Создайте свой профиль",
"onboarding.profile.upload_avatar": "Загрузить фото профиля", "onboarding.profile.upload_avatar": "Загрузить фото профиля",
@ -741,16 +741,16 @@
"refresh": "Обновить", "refresh": "Обновить",
"regeneration_indicator.please_stand_by": "Пожалуйста, подождите.", "regeneration_indicator.please_stand_by": "Пожалуйста, подождите.",
"regeneration_indicator.preparing_your_home_feed": "Готовим вашу ленту…", "regeneration_indicator.preparing_your_home_feed": "Готовим вашу ленту…",
"relative_time.days": "{number} д", "relative_time.days": "{number} д.",
"relative_time.full.days": "{number, plural, one {# день} many {# дней} other {# дня}} назад", "relative_time.full.days": "{number, plural, one {# день} many {# дней} other {# дня}} назад",
"relative_time.full.hours": "{number, plural, one {# час} many {# часов} other {# часа}} назад", "relative_time.full.hours": "{number, plural, one {# час} many {# часов} other {# часа}} назад",
"relative_time.full.just_now": "только что", "relative_time.full.just_now": "только что",
"relative_time.full.minutes": "{number, plural, one {# минуту} many {# минут} other {# минуты}} назад", "relative_time.full.minutes": "{number, plural, one {# минуту} many {# минут} other {# минуты}} назад",
"relative_time.full.seconds": "{number, plural, one {# секунду} many {# секунд} other {# секунды}} назад", "relative_time.full.seconds": "{number, plural, one {# секунду} many {# секунд} other {# секунды}} назад",
"relative_time.hours": "{number} ч", "relative_time.hours": "{number} ч.",
"relative_time.just_now": "только что", "relative_time.just_now": "только что",
"relative_time.minutes": "{number} мин", "relative_time.minutes": "{number} мин.",
"relative_time.seconds": "{number} с", "relative_time.seconds": "{number} с.",
"relative_time.today": "сегодня", "relative_time.today": "сегодня",
"reply_indicator.attachments": "{count, plural, one {# вложение} few {# вложения} other {# вложений}}", "reply_indicator.attachments": "{count, plural, one {# вложение} few {# вложения} other {# вложений}}",
"reply_indicator.cancel": "Отмена", "reply_indicator.cancel": "Отмена",
@ -836,7 +836,7 @@
"server_banner.is_one_of_many": "{domain} — это один из многих независимых серверов Mastodon, которые вы можете использовать, чтобы присоединиться к сети Fediverse.", "server_banner.is_one_of_many": "{domain} — это один из многих независимых серверов Mastodon, которые вы можете использовать, чтобы присоединиться к сети Fediverse.",
"server_banner.server_stats": "Статистика сервера:", "server_banner.server_stats": "Статистика сервера:",
"sign_in_banner.create_account": "Зарегистрироваться", "sign_in_banner.create_account": "Зарегистрироваться",
"sign_in_banner.follow_anyone": "Подписывайтесь на кого угодно в федивёрсе и читайте ленту в хронологическом порядке. Никаких алгоритмов, рекламы или кликбейта.", "sign_in_banner.follow_anyone": "Подписывайтесь на кого угодно в федивёрсе и читайте ленту в хронологическом порядке. Никаких алгоритмов, рекламы и кликбейта.",
"sign_in_banner.mastodon_is": "Mastodon — лучший способ быть в курсе всего происходящего.", "sign_in_banner.mastodon_is": "Mastodon — лучший способ быть в курсе всего происходящего.",
"sign_in_banner.sign_in": "Войти", "sign_in_banner.sign_in": "Войти",
"sign_in_banner.sso_redirect": "Вход/Регистрация", "sign_in_banner.sso_redirect": "Вход/Регистрация",

View File

@ -1857,7 +1857,10 @@ body > [data-popper-placement] {
} }
.status__quote { .status__quote {
--quote-margin: 36px; // --status-gutter-width is currently only set inside of
// .notification-ungrouped, so everywhere else this will fall back
// to the pixel values
--quote-margin: var(--status-gutter-width, 36px);
position: relative; position: relative;
margin-block-start: 16px; margin-block-start: 16px;
@ -1868,7 +1871,7 @@ body > [data-popper-placement] {
border: var(--nested-card-border); border: var(--nested-card-border);
@container (width > 460px) { @container (width > 460px) {
--quote-margin: 56px; --quote-margin: var(--status-gutter-width, 56px);
} }
} }
@ -10817,21 +10820,23 @@ noscript {
} }
} }
.status { .status:not(.status--is-quote) {
border: 0; border: 0;
padding: 0; padding: 0;
&__avatar {
width: 40px;
height: 40px;
}
} }
.status__wrapper-direct { .status__wrapper-direct {
background: transparent; background: transparent;
} }
$icon-margin: 48px; // 40px avatar + 8px gap .status {
// 40px avatar + 8px gap
--status-gutter-width: 48px;
}
.status--is-quote {
--status-gutter-width: 0;
}
.status__content, .status__content,
.status__action-bar, .status__action-bar,
@ -10845,16 +10850,16 @@ noscript {
.hashtag-bar, .hashtag-bar,
.content-warning, .content-warning,
.filter-warning { .filter-warning {
margin-inline-start: $icon-margin; margin-inline-start: var(--status-gutter-width);
width: calc(100% - $icon-margin); width: calc(100% - var(--status-gutter-width));
} }
.more-from-author { .more-from-author {
width: calc(100% - $icon-margin + 2px); width: calc(100% - var(--status-gutter-width) + 2px);
} }
.status__content__read-more-button { .status__content__read-more-button {
margin-inline-start: $icon-margin; margin-inline-start: var(--status-gutter-width);
} }
.notification__report { .notification__report {

View File

@ -1349,6 +1349,8 @@ fa:
basic_information: اطلاعات پایه basic_information: اطلاعات پایه
hint_html: "<strong>شخصی‌سازی آن چه مردم روی نمایهٔ عمومیتان و کنار فرسته‌هایتان می‌بینند.</strong> هنگامی که نمایه‌ای کامل و یک تصویر نمایه داشته باشید،‌ احتمال پی‌گیری متقابل و تعامل با شما بیش‌تر است." hint_html: "<strong>شخصی‌سازی آن چه مردم روی نمایهٔ عمومیتان و کنار فرسته‌هایتان می‌بینند.</strong> هنگامی که نمایه‌ای کامل و یک تصویر نمایه داشته باشید،‌ احتمال پی‌گیری متقابل و تعامل با شما بیش‌تر است."
other: سایر other: سایر
emoji_styles:
auto: خودکار
errors: errors:
'400': درخواستی که فرستادید نامعتبر یا اشتباه بود. '400': درخواستی که فرستادید نامعتبر یا اشتباه بود.
'403': شما اجازهٔ دیدن این صفحه را ندارید. '403': شما اجازهٔ دیدن این صفحه را ندارید.

View File

@ -354,7 +354,7 @@ ru:
enable: Включить enable: Включить
enabled: Включён enabled: Включён
enabled_msg: Эмодзи включён enabled_msg: Эмодзи включён
image_hint: Поддерживаются файлы PNG или GIF размером не более %{size} image_hint: Поддерживаются файлы PNG и GIF размером не более %{size}
list: В список list: В список
listed: В списке listed: В списке
new: new:
@ -1280,7 +1280,7 @@ ru:
confirm: Продолжить confirm: Продолжить
hint_html: "<strong>Подсказка:</strong> В течение часа вам не придётся снова вводить свой пароль." hint_html: "<strong>Подсказка:</strong> В течение часа вам не придётся снова вводить свой пароль."
invalid_password: Неверный пароль invalid_password: Неверный пароль
prompt: Введите пароль для продолжения prompt: Введите пароль, чтобы продолжить
crypto: crypto:
errors: errors:
invalid_key: должен быть действительным Ed25519- или Curve25519-ключом invalid_key: должен быть действительным Ed25519- или Curve25519-ключом
@ -1290,35 +1290,35 @@ ru:
with_month_name: "%d %B %Y" with_month_name: "%d %B %Y"
datetime: datetime:
distance_in_words: distance_in_words:
about_x_hours: "%{count}ч" about_x_hours: "%{count} ч."
about_x_months: "%{count}мес" about_x_months: "%{count} мес."
about_x_years: "%{count}г" about_x_years: "%{count} г."
almost_x_years: "%{count}г" almost_x_years: "%{count} г."
half_a_minute: Только что half_a_minute: Только что
less_than_x_minutes: "%{count}мин" less_than_x_minutes: "%{count} мин."
less_than_x_seconds: Только что less_than_x_seconds: Только что
over_x_years: "%{count}г" over_x_years: "%{count} г."
x_days: "%{count}д" x_days: "%{count} д."
x_minutes: "%{count}мин" x_minutes: "%{count} мин."
x_months: "%{count}мес" x_months: "%{count} мес."
x_seconds: "%{count}сек" x_seconds: "%{count} с."
deletes: deletes:
challenge_not_passed: Введённая вами информация некорректна challenge_not_passed: Данные введены неверно
confirm_password: Введите свой пароль, чтобы подтвердить, что вы — это вы, и никто другой confirm_password: Введите свой пароль, чтобы подтвердить, что это ваша учётная запись
confirm_username: Введите своё имя пользователя для подтверждения confirm_username: Введите своё имя пользователя для подтверждения
proceed: Удалить учётную запись proceed: Удалить учётную запись
success_msg: Ваша учётная запись была успешно удалена success_msg: Ваша учётная запись удалена
warning: warning:
before: 'Внимательно прочитайте следующую информацию перед началом:' before: 'Внимательно ознакомьтесь со следующими замечаниями перед тем как продолжить:'
caches: Некоторые данные, обработанные другими узлами, однако, могут храниться ещё какое-то время caches: На других серверах могут остаться сохранённые в кэше данные
data_removal: Все ваши золотые посты, шикарный профиль и прочие данные будут безвозвратно уничтожены data_removal: Все ваши посты и другие ваши данные будут безвозвратно уничтожены
email_change_html: Вы можете <a href="%{path}">изменить свой адрес электронной почты</a>, не удаляя свою учетную запись email_change_html: Вы можете <a href="%{path}">изменить свой адрес электронной почты</a>, не удаляя свою учетную запись
email_contact_html: Если оно все еще не пришло, вы можете обратиться за помощью по электронной почте <a href="mailto:%{email}">%{email}</a> email_contact_html: Если оно все еще не пришло, вы можете обратиться за помощью по электронной почте <a href="mailto:%{email}">%{email}</a>
email_reconfirmation_html: Если вы не получили подтверждение по электронной почте, вы можете <a href="%{path}">запросить его снова</a> email_reconfirmation_html: Если вы не получили подтверждение по электронной почте, вы можете <a href="%{path}">запросить его снова</a>
irreversible: После удаления восстановить или повторно активировать учётную запись не получится irreversible: После удаления вы больше не сможете ни восстановить, ни повторно активировать свою учётную запись
more_details_html: За всеми подробностями, изучите <a href="%{terms_path}">политику конфиденциальности</a>. more_details_html: За более подробной информацией вы можете обратиться к <a href="%{terms_path}">политике конфиденциальности</a>.
username_available: Ваше имя пользователя снова станет доступным username_available: Ваше имя пользователя снова станет доступным
username_unavailable: Ваше имя пользователя останется недоступным для использования username_unavailable: Зарегистрироваться с вашим именем пользователя будет невозможно
disputes: disputes:
strikes: strikes:
action_taken: Предпринятые меры action_taken: Предпринятые меры
@ -1353,6 +1353,10 @@ ru:
basic_information: Основная информация basic_information: Основная информация
hint_html: "<strong>Настройте то, что люди видят в вашем публичном профиле и рядом с вашими сообщениями.</strong> Другие люди с большей вероятностью подпишутся на Вас и будут взаимодействовать с вами, если у Вас заполнен профиль и добавлено изображение." hint_html: "<strong>Настройте то, что люди видят в вашем публичном профиле и рядом с вашими сообщениями.</strong> Другие люди с большей вероятностью подпишутся на Вас и будут взаимодействовать с вами, если у Вас заполнен профиль и добавлено изображение."
other: Прочее other: Прочее
emoji_styles:
auto: Автоматически
native: Как в системе
twemoji: Twemoji
errors: errors:
'400': Ваш запрос был недействительным или неправильным. '400': Ваш запрос был недействительным или неправильным.
'403': У Вас нет доступа к просмотру этой страницы. '403': У Вас нет доступа к просмотру этой страницы.
@ -2136,17 +2140,17 @@ ru:
webauthn_credentials: webauthn_credentials:
add: Добавить новый электронный ключ add: Добавить новый электронный ключ
create: create:
error: Возникла проблема с добавлением ключа безопасности. Пожалуйста, попробуйте еще раз. error: При добавлении электронного ключа произошла ошибка. Попробуйте ещё раз.
success: Ваш электронный ключ добавлен. success: Ваш электронный ключ добавлен.
delete: Удалить delete: Удалить
delete_confirmation: Вы действительно хотите удалить этот электронный ключ? delete_confirmation: Вы действительно хотите удалить этот электронный ключ?
description_html: Если вы включите <strong>аутентификацию по электронным ключам</strong>, для входа в учётную запись вам будет предложено использовать один из ваших ключей. description_html: Если вы включите <strong>аутентификацию по электронным ключам</strong>, для входа в учётную запись вам будет предложено использовать один из ваших ключей.
destroy: destroy:
error: Произошла ошибка при удалении ключа безопасности. Пожалуйста, попробуйте еще раз. error: При удалении электронного ключа произошла ошибка. Попробуйте ещё раз.
success: Ваш электронный ключ удалён. success: Ваш электронный ключ удалён.
invalid_credential: Неверный электронный ключ invalid_credential: Неверный электронный ключ
nickname_hint: Введите название для нового электронного ключа nickname_hint: Введите название для нового электронного ключа
not_enabled: Вы еще не включили WebAuthn not_enabled: Вы еще не включили WebAuthn
not_supported: Этот браузер не поддерживает ключи безопасности not_supported: В этом браузере отсутствует поддержка электронных ключей
otp_required: Чтобы использовать ключи безопасности, сначала включите двухфакторную аутентификацию. otp_required: Чтобы использовать электронные ключи, сначала включите двухфакторную аутентификацию.
registered_on: Зарегистрирован %{date} registered_on: Зарегистрирован %{date}

View File

@ -149,6 +149,9 @@ ca:
min_age: No hauria de ser inferior a l'edat mínima exigida per la llei de la vostra jurisdicció. min_age: No hauria de ser inferior a l'edat mínima exigida per la llei de la vostra jurisdicció.
user: user:
chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques chosen_languages: Quan estigui marcat, només es mostraran els tuts de les llengües seleccionades en les línies de temps públiques
date_of_birth:
one: Ens hem d'assegurar que teniu com a mínim %{count} any per a fer servir %{domain}. No ho desarem.
other: Ens hem d'assegurar que teniu com a mínim %{count} anys per a fer servir %{domain}. No ho desarem.
role: El rol controla quins permisos té l'usuari. role: El rol controla quins permisos té l'usuari.
user_role: user_role:
color: Color que s'usarà per al rol a tota la interfície d'usuari, com a RGB en format hexadecimal color: Color que s'usarà per al rol a tota la interfície d'usuari, com a RGB en format hexadecimal

View File

@ -150,6 +150,11 @@ cs:
min_age: Neměla by být pod minimálním věkem požadovaným zákony vaší jurisdikce. min_age: Neměla by být pod minimálním věkem požadovaným zákony vaší jurisdikce.
user: user:
chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích chosen_languages: Po zaškrtnutí budou ve veřejných časových osách zobrazeny pouze příspěvky ve zvolených jazycích
date_of_birth:
few: Musíme se ujistit, že je Vám alespoň %{count}, abyste mohli používat %{domain}. Nebudeme to ukládat.
many: Musíme se ujistit, že je Vám alespoň %{count} let, abyste mohli používat %{domain}. Nebudeme to ukládat.
one: Musíme se ujistit, že je Vám alespoň %{count} rok, abyste mohli používat %{domain}. Nebudeme to ukládat.
other: Musíme se ujistit, že je Vám alespoň %{count}, abyste mohli používat %{domain}. Nebudeme to ukládat.
role: Role určuje, která oprávnění uživatel má. role: Role určuje, která oprávnění uživatel má.
user_role: user_role:
color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu color: Barva, která má být použita pro roli v celém UI, jako RGB v hex formátu

View File

@ -151,8 +151,8 @@ da:
user: user:
chosen_languages: Når markeret, vil kun indlæg på de valgte sprog fremgå på offentlige tidslinjer chosen_languages: Når markeret, vil kun indlæg på de valgte sprog fremgå på offentlige tidslinjer
date_of_birth: date_of_birth:
one: Vi skal sikre os, at du er mindst %{count} for at kunne bruge %{domain}. Vi gemmer ikke dette. one: Vi skal sikre os, at du er mindst %{count} for at kunne bruge %{domain}. Informationen gemmes ikke.
other: Vi skal sikre os, at du er mindst %{count} for at kunne bruge %{domain}. Vi gemmer ikke dette. other: Vi skal sikre os, at du er mindst %{count} for at kunne bruge %{domain}. Informationen gemmes ikke.
role: Rollen styrer, hvilke tilladelser brugeren er tildelt. role: Rollen styrer, hvilke tilladelser brugeren er tildelt.
user_role: user_role:
color: Farven, i RGB hex-format, der skal bruges til rollen i hele UI'en color: Farven, i RGB hex-format, der skal bruges til rollen i hele UI'en

View File

@ -150,6 +150,9 @@ fo:
min_age: Eigur ikki at vera undir lægsta aldri, sum lógirnar í tínum rættarøki krevja. min_age: Eigur ikki at vera undir lægsta aldri, sum lógirnar í tínum rættarøki krevja.
user: user:
chosen_languages: Tá hetta er valt, verða einans postar í valdum málum vístir á almennum tíðarlinjum chosen_languages: Tá hetta er valt, verða einans postar í valdum málum vístir á almennum tíðarlinjum
date_of_birth:
one: Vit mugu tryggja okkum, at tú er í minsta lagi %{count} fyri at brúka %{domain}. Vit goyma ikki hesar upplýsingar.
other: Vit mugu tryggja okkum, at tú er í minsta lagi %{count} ár fyri at brúka %{domain}. Vit goyma ikki hesar upplýsingar.
role: Leikluturin stýrir hvørji rættindi, brúkarin hevur. role: Leikluturin stýrir hvørji rættindi, brúkarin hevur.
user_role: user_role:
color: Litur, sum leikluturin hevur í øllum brúkaramarkamótinum, sum RGB og upplýst sum sekstandatal color: Litur, sum leikluturin hevur í øllum brúkaramarkamótinum, sum RGB og upplýst sum sekstandatal

View File

@ -150,6 +150,9 @@ fy:
min_age: Mei net leger wêze as de minimale fereaske leeftiid neffens de wetten fan jo jurisdiksje. min_age: Mei net leger wêze as de minimale fereaske leeftiid neffens de wetten fan jo jurisdiksje.
user: user:
chosen_languages: Allinnich berjochten yn de selektearre talen wurde op de iepenbiere tiidline toand chosen_languages: Allinnich berjochten yn de selektearre talen wurde op de iepenbiere tiidline toand
date_of_birth:
one: Wy moatte derfoar soargje dat jo op syn minst %{count} binne om %{domain} brûke te meien. Dit wurdt net bewarre.
other: Wy moatte derfoar soargje dat jo op syn minst %{count} binne om %{domain} brûke te meien. Dit wurdt net bewarre.
role: De rol bepaalt hokker rjochten in brûker hat. role: De rol bepaalt hokker rjochten in brûker hat.
user_role: user_role:
color: Kleur dyt brûkt wurdt foar de rol yn de UI, as RGB yn heksadesimaal formaat color: Kleur dyt brûkt wurdt foar de rol yn de UI, as RGB yn heksadesimaal formaat

View File

@ -150,6 +150,9 @@ gl:
min_age: Non debería ser inferior á idade mínima requerida polas leis da túa xurisdición. min_age: Non debería ser inferior á idade mínima requerida polas leis da túa xurisdición.
user: user:
chosen_languages: Se ten marca, só as publicacións nos idiomas seleccionados serán mostrados en cronoloxías públicas chosen_languages: Se ten marca, só as publicacións nos idiomas seleccionados serán mostrados en cronoloxías públicas
date_of_birth:
one: Temos que confirmar que tes %{count} anos polo menos para usar %{domain}. Non gardamos este dato.
other: Temos que confirmar que tes %{count} anos polo menos para usar %{domain}. Non gardamos este dato.
role: Os roles establecen os permisos que ten a usuaria. role: Os roles establecen os permisos que ten a usuaria.
user_role: user_role:
color: Cor que se usará para o rol a través da IU, como RGB en formato hex color: Cor que se usará para o rol a través da IU, como RGB en formato hex

View File

@ -150,6 +150,9 @@ is:
min_age: Ætti ekki að vera lægri en sá lágmarksaldur sek kveðið er á um í lögum þíns lögsagnarumdæmis. min_age: Ætti ekki að vera lægri en sá lágmarksaldur sek kveðið er á um í lögum þíns lögsagnarumdæmis.
user: user:
chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum
date_of_birth:
one: Við verðum að ganga úr skugga um að þú hafir náð %{count} aldri til að nota %{domain}. Við munum ekki geyma þessar upplýsingar.
other: Við verðum að ganga úr skugga um að þú hafir náð %{count} aldri til að nota %{domain}. Við munum ekki geyma þessar upplýsingar.
role: Hlutverk stýrir hvaða heimildir notandinn hefur. role: Hlutverk stýrir hvaða heimildir notandinn hefur.
user_role: user_role:
color: Litur sem notaður er fyrir hlutverkið allsstaðar í viðmótinu, sem RGB-gildi á hex-sniði color: Litur sem notaður er fyrir hlutverkið allsstaðar í viðmótinu, sem RGB-gildi á hex-sniði

View File

@ -150,6 +150,9 @@ it:
min_age: Non si dovrebbe avere un'età inferiore a quella minima richiesta, dalle leggi della tua giurisdizione. min_age: Non si dovrebbe avere un'età inferiore a quella minima richiesta, dalle leggi della tua giurisdizione.
user: user:
chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate chosen_languages: Quando una o più lingue sono contrassegnate, nelle timeline pubbliche vengono mostrati solo i toot nelle lingue selezionate
date_of_birth:
one: Dobbiamo assicurarci che tu abbia almeno %{count} anni per utilizzare %{domain}. Non memorizzeremo questo dato.
other: Dobbiamo assicurarci che tu abbia almeno %{count} anni per utilizzare %{domain}. Non memorizzeremo questo dato.
role: Il ruolo controlla quali permessi ha l'utente. role: Il ruolo controlla quali permessi ha l'utente.
user_role: user_role:
color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale color: Colore da usare per il ruolo in tutta l'UI, come RGB in formato esadecimale

View File

@ -8,7 +8,7 @@ ru:
display_name: Ваше полное имя или псевдоним. display_name: Ваше полное имя или псевдоним.
fields: Домашняя страница, местоимения, возраст — всё что угодно. fields: Домашняя страница, местоимения, возраст — всё что угодно.
indexable: Отметьте флажок, чтобы ваши публичные посты могли быть найдены при помощи поиска в Mastodon. Люди, которые взаимодействовали с вашими постами, смогут их найти вне зависимости от этой настройки. indexable: Отметьте флажок, чтобы ваши публичные посты могли быть найдены при помощи поиска в Mastodon. Люди, которые взаимодействовали с вашими постами, смогут их найти вне зависимости от этой настройки.
note: 'Вы можете @упоминать других людей или использовать #хештеги.' note: 'Вы можете @упоминать других людей, а также использовать #хештеги.'
show_collections: Отметьте флажок, чтобы кто угодно мог просматривать списки ваших подписок и подписчиков. Люди, на которых вы подписаны, будут знать о том, что вы на них подписаны, вне зависимости от этой настройки. show_collections: Отметьте флажок, чтобы кто угодно мог просматривать списки ваших подписок и подписчиков. Люди, на которых вы подписаны, будут знать о том, что вы на них подписаны, вне зависимости от этой настройки.
unlocked: 'Отметьте флажок, чтобы на вас можно было подписаться, не запрашивая подтверждения. Снимите флажок, чтобы вы могли просматривать запросы на подписку и выбирать: принять или отклонить новых подписчиков.' unlocked: 'Отметьте флажок, чтобы на вас можно было подписаться, не запрашивая подтверждения. Снимите флажок, чтобы вы могли просматривать запросы на подписку и выбирать: принять или отклонить новых подписчиков.'
account_alias: account_alias:

View File

@ -22,7 +22,21 @@ export function MastodonThemes(): Plugin {
projectRoot = userConfig.envDir; projectRoot = userConfig.envDir;
jsRoot = userConfig.root; jsRoot = userConfig.root;
const entrypoints: Record<string, string> = {}; let entrypoints: Record<string, string> = {};
const existingInputs = userConfig.build?.rollupOptions?.input;
if (typeof existingInputs === 'string') {
entrypoints[path.basename(existingInputs)] = existingInputs;
} else if (Array.isArray(existingInputs)) {
for (const input of existingInputs) {
if (typeof input === 'string') {
entrypoints[path.basename(input)] = input;
}
}
} else if (typeof existingInputs === 'object') {
entrypoints = existingInputs;
}
// Get all files mentioned in the themes.yml file. // Get all files mentioned in the themes.yml file.
const themes = await loadThemesFromConfig(projectRoot); const themes = await loadThemesFromConfig(projectRoot);

View File

@ -30,7 +30,7 @@
"test:storybook": "vitest --project=storybook", "test:storybook": "vitest --project=storybook",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "VITE_RUBY_PUBLIC_OUTPUT_DIR='.' VITE_RUBY_PUBLIC_DIR='./storybook-static' storybook build", "build-storybook": "storybook build",
"chromatic": "npx chromatic -d storybook-static" "chromatic": "npx chromatic -d storybook-static"
}, },
"repository": { "repository": {
@ -69,6 +69,7 @@
"emojibase": "^16.0.0", "emojibase": "^16.0.0",
"emojibase-data": "^16.0.3", "emojibase-data": "^16.0.3",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fast-glob": "^3.3.3",
"fuzzysort": "^3.0.0", "fuzzysort": "^3.0.0",
"history": "^4.10.1", "history": "^4.10.1",
"hoist-non-react-statics": "^3.3.2", "hoist-non-react-statics": "^3.3.2",
@ -105,6 +106,7 @@
"redux-immutable": "^4.0.0", "redux-immutable": "^4.0.0",
"regenerator-runtime": "^0.14.0", "regenerator-runtime": "^0.14.0",
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"rollup-plugin-gzip": "^4.1.1",
"rollup-plugin-visualizer": "^6.0.0", "rollup-plugin-visualizer": "^6.0.0",
"sass": "^1.62.1", "sass": "^1.62.1",
"stacktrace-js": "^2.0.2", "stacktrace-js": "^2.0.2",
@ -115,9 +117,8 @@
"twitter-text": "3.1.0", "twitter-text": "3.1.0",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-manifest-sri": "^0.2.0",
"vite-plugin-pwa": "^1.0.0", "vite-plugin-pwa": "^1.0.0",
"vite-plugin-rails": "^0.5.0",
"vite-plugin-ruby": "^5.1.1",
"vite-plugin-static-copy": "^3.1.0", "vite-plugin-static-copy": "^3.1.0",
"vite-plugin-svgr": "^4.3.0", "vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
@ -187,15 +188,12 @@
"stylelint-config-standard-scss": "^15.0.1", "stylelint-config-standard-scss": "^15.0.1",
"typescript": "~5.7.3", "typescript": "~5.7.3",
"typescript-eslint": "^8.29.1", "typescript-eslint": "^8.29.1",
"vite-plugin-rails": "^0.5.0",
"vite-plugin-svgr": "^4.2.0",
"vitest": "^3.2.1" "vitest": "^3.2.1"
}, },
"resolutions": { "resolutions": {
"@types/react": "^18.2.7", "@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"kind-of": "^6.0.3", "kind-of": "^6.0.3",
"vite-plugin-ruby": "^5.1.0",
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {

View File

@ -1,19 +1,25 @@
import path from 'node:path'; import path from 'node:path';
import { readdir } from 'node:fs/promises';
import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin'; import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
import legacy from '@vitejs/plugin-legacy'; import legacy from '@vitejs/plugin-legacy';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { PluginOption } from 'vite'; import glob from 'fast-glob';
import postcssPresetEnv from 'postcss-preset-env';
import Compress from 'rollup-plugin-gzip';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
import {
PluginOption,
defineConfig,
UserConfigFnPromise,
UserConfig,
} from 'vite';
import manifestSRI from 'vite-plugin-manifest-sri';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import RailsPlugin from 'vite-plugin-rails';
import { viteStaticCopy } from 'vite-plugin-static-copy'; import { viteStaticCopy } from 'vite-plugin-static-copy';
import svgr from 'vite-plugin-svgr'; import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite';
import postcssPresetEnv from 'postcss-preset-env';
import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales'; import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed'; import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed';
import { MastodonThemes } from './config/vite/plugin-mastodon-themes'; import { MastodonThemes } from './config/vite/plugin-mastodon-themes';
@ -22,8 +28,26 @@ import { MastodonNameLookup } from './config/vite/plugin-name-lookup';
const jsRoot = path.resolve(__dirname, 'app/javascript'); const jsRoot = path.resolve(__dirname, 'app/javascript');
export const config: UserConfigFnPromise = async ({ mode, command }) => { export const config: UserConfigFnPromise = async ({ mode, command }) => {
const isProdBuild = mode === 'production' && command === 'build';
let outDirName = 'packs-dev';
if (mode === 'test') {
outDirName = 'packs-test';
} else if (mode === 'production') {
outDirName = 'packs';
}
const outDir = path.resolve('public', outDirName);
return { return {
root: jsRoot, root: jsRoot,
base: `/${outDirName}/`,
envDir: __dirname,
resolve: {
alias: {
'~/': `${jsRoot}/`,
'@/': `${jsRoot}/`,
},
},
css: { css: {
postcss: { postcss: {
plugins: [ plugins: [
@ -41,12 +65,18 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
// but it needs to be scoped to the whole domain // but it needs to be scoped to the whole domain
'Service-Worker-Allowed': '/', 'Service-Worker-Allowed': '/',
}, },
port: 3036,
}, },
build: { build: {
commonjsOptions: { transformMixedEsModules: true }, commonjsOptions: { transformMixedEsModules: true },
chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB
sourcemap: true, sourcemap: true,
emptyOutDir: mode !== 'production',
manifest: true,
outDir,
assetsDir: 'assets',
rollupOptions: { rollupOptions: {
input: await findEntrypoints(),
output: { output: {
chunkFileNames({ facadeModuleId, name }) { chunkFileNames({ facadeModuleId, name }) {
if (!facadeModuleId) { if (!facadeModuleId) {
@ -84,18 +114,12 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
}, },
plugins: [ plugins: [
tsconfigPaths({ projects: [path.resolve(__dirname, 'tsconfig.json')] }), tsconfigPaths({ projects: [path.resolve(__dirname, 'tsconfig.json')] }),
RailsPlugin({
compress: mode === 'production' && command === 'build',
sri: {
manifestPaths: ['.vite/manifest.json', '.vite/manifest-assets.json'],
},
}),
MastodonThemes(),
react({ react({
babel: { babel: {
plugins: ['formatjs', 'transform-react-remove-prop-types'], plugins: ['formatjs', 'transform-react-remove-prop-types'],
}, },
}), }),
MastodonThemes(),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [
{ {
@ -117,8 +141,13 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
renderLegacyChunks: false, renderLegacyChunks: false,
modernPolyfills: true, modernPolyfills: true,
}), }),
isProdBuild && (Compress() as PluginOption),
command === 'build' &&
manifestSRI({
manifestPaths: ['.vite/manifest.json', '.vite/manifest-assets.json'],
}),
VitePWA({ VitePWA({
srcDir: 'mastodon/service_worker', srcDir: path.resolve(jsRoot, 'mastodon/service_worker'),
// We need to use injectManifest because we use our own service worker // We need to use injectManifest because we use our own service worker
strategies: 'injectManifest', strategies: 'injectManifest',
manifest: false, manifest: false,
@ -150,4 +179,54 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
} satisfies UserConfig; } satisfies UserConfig;
}; };
async function findEntrypoints() {
const entrypoints: Record<string, string> = {};
// First, JS entrypoints
const jsEntrypoints = await readdir(path.resolve(jsRoot, 'entrypoints'), {
withFileTypes: true,
});
const jsExtTest = /\.[jt]sx?$/;
for (const file of jsEntrypoints) {
if (file.isFile() && jsExtTest.test(file.name)) {
entrypoints[file.name.replace(jsExtTest, '')] = path.resolve(
file.parentPath,
file.name,
);
}
}
// Next, SCSS entrypoints
const scssEntrypoints = await readdir(
path.resolve(jsRoot, 'styles/entrypoints'),
{ withFileTypes: true },
);
const scssExtTest = /\.s?css$/;
for (const file of scssEntrypoints) {
if (file.isFile() && scssExtTest.test(file.name)) {
entrypoints[file.name.replace(scssExtTest, '')] = path.resolve(
file.parentPath,
file.name,
);
}
}
// Lastly other assets
const assetEntrypoints = await glob('{fonts,icons,images}/**/*', {
cwd: jsRoot,
absolute: true,
});
const excludeExts = ['', '.md'];
for (const file of assetEntrypoints) {
const ext = path.extname(file);
if (excludeExts.includes(ext)) {
continue;
}
const name = path.basename(file);
entrypoints[name] = path.resolve(jsRoot, file);
}
return entrypoints;
}
export default defineConfig(config); export default defineConfig(config);

View File

@ -1,9 +1,6 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
import { import {
configDefaults, configDefaults,
defineConfig, defineConfig,
@ -13,15 +10,13 @@ import {
import { config as viteConfig } from './vite.config.mjs'; import { config as viteConfig } from './vite.config.mjs';
const storybookTests: TestProjectInlineConfiguration = { const storybookTests: TestProjectInlineConfiguration = {
extends: true,
plugins: [ plugins: [
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({ storybookTest({
configDir: '.storybook', configDir: '.storybook',
storybookScript: 'yarn run storybook', storybookScript: 'yarn run storybook',
}), }),
react(),
svgr(),
tsconfigPaths(),
], ],
test: { test: {
name: 'storybook', name: 'storybook',

113
yarn.lock
View File

@ -2680,6 +2680,7 @@ __metadata:
eslint-plugin-react: "npm:^7.37.4" eslint-plugin-react: "npm:^7.37.4"
eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-react-hooks: "npm:^5.2.0"
eslint-plugin-storybook: "npm:^9.0.4" eslint-plugin-storybook: "npm:^9.0.4"
fast-glob: "npm:^3.3.3"
fuzzysort: "npm:^3.0.0" fuzzysort: "npm:^3.0.0"
globals: "npm:^16.0.0" globals: "npm:^16.0.0"
history: "npm:^4.10.1" history: "npm:^4.10.1"
@ -2724,6 +2725,7 @@ __metadata:
redux-immutable: "npm:^4.0.0" redux-immutable: "npm:^4.0.0"
regenerator-runtime: "npm:^0.14.0" regenerator-runtime: "npm:^0.14.0"
requestidlecallback: "npm:^0.3.0" requestidlecallback: "npm:^0.3.0"
rollup-plugin-gzip: "npm:^4.1.1"
rollup-plugin-visualizer: "npm:^6.0.0" rollup-plugin-visualizer: "npm:^6.0.0"
sass: "npm:^1.62.1" sass: "npm:^1.62.1"
stacktrace-js: "npm:^2.0.2" stacktrace-js: "npm:^2.0.2"
@ -2740,11 +2742,10 @@ __metadata:
typescript-eslint: "npm:^8.29.1" typescript-eslint: "npm:^8.29.1"
use-debounce: "npm:^10.0.0" use-debounce: "npm:^10.0.0"
vite: "npm:^6.3.5" vite: "npm:^6.3.5"
vite-plugin-manifest-sri: "npm:^0.2.0"
vite-plugin-pwa: "npm:^1.0.0" vite-plugin-pwa: "npm:^1.0.0"
vite-plugin-rails: "npm:^0.5.0"
vite-plugin-ruby: "npm:^5.1.1"
vite-plugin-static-copy: "npm:^3.1.0" vite-plugin-static-copy: "npm:^3.1.0"
vite-plugin-svgr: "npm:^4.2.0" vite-plugin-svgr: "npm:^4.3.0"
vite-tsconfig-paths: "npm:^5.1.4" vite-tsconfig-paths: "npm:^5.1.4"
vitest: "npm:^3.2.1" vitest: "npm:^3.2.1"
wicg-inert: "npm:^3.1.2" wicg-inert: "npm:^3.1.2"
@ -3298,7 +3299,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0": "@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.2, @rollup/pluginutils@npm:^5.1.0":
version: 5.1.4 version: 5.1.4
resolution: "@rollup/pluginutils@npm:5.1.4" resolution: "@rollup/pluginutils@npm:5.1.4"
dependencies: dependencies:
@ -3314,6 +3315,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@rollup/pluginutils@npm:^5.1.3":
version: 5.2.0
resolution: "@rollup/pluginutils@npm:5.2.0"
dependencies:
"@types/estree": "npm:^1.0.0"
estree-walker: "npm:^2.0.2"
picomatch: "npm:^4.0.2"
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
checksum: 10c0/794890d512751451bcc06aa112366ef47ea8f9125dac49b1abf72ff8b079518b09359de9c60a013b33266541634e765ae61839c749fae0edb59a463418665c55
languageName: node
linkType: hard
"@rollup/rollup-android-arm-eabi@npm:4.40.2": "@rollup/rollup-android-arm-eabi@npm:4.40.2":
version: 4.40.2 version: 4.40.2
resolution: "@rollup/rollup-android-arm-eabi@npm:4.40.2" resolution: "@rollup/rollup-android-arm-eabi@npm:4.40.2"
@ -6281,7 +6298,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1": "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1":
version: 4.4.1 version: 4.4.1
resolution: "debug@npm:4.4.1" resolution: "debug@npm:4.4.1"
dependencies: dependencies:
@ -10223,7 +10240,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": "picocolors@npm:^1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "picocolors@npm:1.1.1" resolution: "picocolors@npm:1.1.1"
checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58
@ -11800,12 +11817,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rollup-plugin-gzip@npm:^3.1.0": "rollup-plugin-gzip@npm:^4.1.1":
version: 3.1.2 version: 4.1.1
resolution: "rollup-plugin-gzip@npm:3.1.2" resolution: "rollup-plugin-gzip@npm:4.1.1"
peerDependencies: peerDependencies:
rollup: ">=2.0.0" rollup: ">=2.0.0"
checksum: 10c0/5129d3970cca37bfb5a2fdeddb863bc76be12489ec0a6fcb2be2764902aa2f8548eb8e6532c4e15912d95e8baaa7391a5ed6b58790ed2529c86a98fa75467edc checksum: 10c0/0ad79a6eb84bb8d88db15a184ca661f44aa6fb3412c98d6a97f1dec365db37945a84c3a2d0bf709ae605ae305a40a0021b2e6d5494c537b029759f3695d9ac96
languageName: node languageName: node
linkType: hard linkType: hard
@ -12536,13 +12553,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"stimulus-vite-helpers@npm:^3.0.0":
version: 3.1.0
resolution: "stimulus-vite-helpers@npm:3.1.0"
checksum: 10c0/828252f43b238191d71b7b4d2048b7df9845c789963a0a23ea0979941e55ad0e14d2b98646eba328e9f4432cf0c0c8340830c5cde1fc9046077c6f1109b4a671
languageName: node
linkType: hard
"storybook@npm:^9.0.4": "storybook@npm:^9.0.4":
version: 9.0.4 version: 9.0.4
resolution: "storybook@npm:9.0.4" resolution: "storybook@npm:9.0.4"
@ -13828,25 +13838,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-plugin-environment@npm:^1.1.3":
version: 1.1.3
resolution: "vite-plugin-environment@npm:1.1.3"
peerDependencies:
vite: ">= 2.7"
checksum: 10c0/225986450220bdc6b109be4d05deeb94013d41cc235fe3064bd6c5a1b33c047ba59cac3a34aa240ae735fee6a77ab9ce033053c5ab7c152497bd7136bd3f3a6d
languageName: node
linkType: hard
"vite-plugin-full-reload@npm:^1.1.0":
version: 1.1.0
resolution: "vite-plugin-full-reload@npm:1.1.0"
dependencies:
picocolors: "npm:^1.0.0"
picomatch: "npm:^2.3.1"
checksum: 10c0/f33ccb4c58051e43b7d261d60f0078c0e28c49631dd86218cfa1902e0a61f038d1f6839f64a4fb95da0445720612d75656eb9b3d13c8b50d336e2548251c54b8
languageName: node
linkType: hard
"vite-plugin-manifest-sri@npm:^0.2.0": "vite-plugin-manifest-sri@npm:^0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "vite-plugin-manifest-sri@npm:0.2.0" resolution: "vite-plugin-manifest-sri@npm:0.2.0"
@ -13875,34 +13866,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-plugin-rails@npm:^0.5.0":
version: 0.5.0
resolution: "vite-plugin-rails@npm:0.5.0"
dependencies:
rollup-plugin-gzip: "npm:^3.1.0"
vite-plugin-environment: "npm:^1.1.3"
vite-plugin-full-reload: "npm:^1.1.0"
vite-plugin-manifest-sri: "npm:^0.2.0"
vite-plugin-ruby: "npm:^5.0.0"
vite-plugin-stimulus-hmr: "npm:^3.0.0"
peerDependencies:
vite: ">=5.0.0"
checksum: 10c0/c1648e87326527ed92339d10f46b7745849a4b1374ed3581410cbd43d9f3ab7aaf4a9285644d2c70a206d8b8330b5949ad69fbe2a2f616b8d8dbec447d75c366
languageName: node
linkType: hard
"vite-plugin-ruby@npm:^5.1.0":
version: 5.1.1
resolution: "vite-plugin-ruby@npm:5.1.1"
dependencies:
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
peerDependencies:
vite: ">=5.0.0"
checksum: 10c0/c14230fef77eb8890897ac71dc56637d49dae8fe5bdb16dcb8fb0d7b7ca068ed30f61940b4ebb0906d03068555156237a84550ec227acde133573078114067ee
languageName: node
linkType: hard
"vite-plugin-static-copy@npm:^3.1.0": "vite-plugin-static-copy@npm:^3.1.0":
version: 3.1.1 version: 3.1.1
resolution: "vite-plugin-static-copy@npm:3.1.1" resolution: "vite-plugin-static-copy@npm:3.1.1"
@ -13918,26 +13881,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-plugin-stimulus-hmr@npm:^3.0.0": "vite-plugin-svgr@npm:^4.3.0":
version: 3.0.0 version: 4.3.0
resolution: "vite-plugin-stimulus-hmr@npm:3.0.0" resolution: "vite-plugin-svgr@npm:4.3.0"
dependencies: dependencies:
debug: "npm:^4.3" "@rollup/pluginutils": "npm:^5.1.3"
stimulus-vite-helpers: "npm:^3.0.0"
checksum: 10c0/964e9713a7402cac0b8a868d7075a35a4a5502ffd11d227aa869da85ab07345af6fc725316bcaf241108076acc0151532f8b3fad6a32225bb279a99e383a2a0c
languageName: node
linkType: hard
"vite-plugin-svgr@npm:^4.2.0":
version: 4.2.0
resolution: "vite-plugin-svgr@npm:4.2.0"
dependencies:
"@rollup/pluginutils": "npm:^5.0.5"
"@svgr/core": "npm:^8.1.0" "@svgr/core": "npm:^8.1.0"
"@svgr/plugin-jsx": "npm:^8.1.0" "@svgr/plugin-jsx": "npm:^8.1.0"
peerDependencies: peerDependencies:
vite: ^2.6.0 || 3 || 4 || 5 vite: ">=2.6.0"
checksum: 10c0/0a6400f20905f53d08f1ce7d1f22d9a57db403e110e790f80c2e0411a0064a071a36b781f56f6823654f98052219171003f9ea023d4a31d930b4a4fc01776d1f checksum: 10c0/a73f10d319f72cd8c16bf9701cf18170f2300f98c72c6bf939565de0b1e93916bd70c6f5a446dc034b4405c72d382655c7c16be4bd1cbf35bbcde5febf7aeffc
languageName: node languageName: node
linkType: hard linkType: hard