Merge commit '0004ed4c8021ebd400d32eb32a5e479ebd7bcf91' into glitch-soc/merge-upstream

This commit is contained in:
Claire 2025-11-30 16:40:18 +01:00
commit 44e6abe48b
52 changed files with 806 additions and 623 deletions

View File

@ -40,7 +40,7 @@ gem 'net-ldap', '~> 0.18'
gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.8.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-rails_csrf_protection', '~> 2.0'
gem 'omniauth-saml', '~> 2.0'
gem 'color_diff', '~> 0.1'
@ -71,7 +71,7 @@ gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'premailer-rails'
gem 'public_suffix', '~> 6.0'
gem 'public_suffix', '~> 7.0'
gem 'pundit', '~> 2.3'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', require: 'rack/cors'

View File

@ -86,8 +86,8 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.20.0)
@ -97,7 +97,7 @@ GEM
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1186.0)
aws-sdk-core (3.239.1)
aws-sdk-core (3.239.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -167,7 +167,7 @@ GEM
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
connection_pool (2.5.5)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -324,7 +324,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.1.1)
i18n-tasks (1.1.2)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -481,7 +481,7 @@ GEM
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
omniauth-rails_csrf_protection (1.0.2)
omniauth-rails_csrf_protection (2.0.0)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.2.4)
@ -542,15 +542,15 @@ GEM
opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-excon (0.26.0)
opentelemetry-instrumentation-excon (0.26.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-faraday (0.30.0)
opentelemetry-instrumentation-faraday (0.30.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.27.0)
opentelemetry-instrumentation-http (0.27.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http_client (0.26.0)
opentelemetry-instrumentation-http_client (0.26.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-net_http (0.26.0)
opentelemetry-instrumentation-net_http (0.26.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-pg (0.33.0)
opentelemetry-helpers-sql
@ -618,7 +618,7 @@ GEM
psych (5.2.6)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.0)
puma (7.1.0)
nio4r (~> 2.0)
pundit (2.5.2)
@ -638,7 +638,7 @@ GEM
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (4.1.1)
rack-protection (4.2.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
@ -672,7 +672,7 @@ GEM
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.0.2)
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.3)
@ -717,10 +717,10 @@ GEM
rotp (6.3.0)
rouge (4.6.1)
rpam2 (4.0.2)
rqrcode (3.1.0)
rqrcode (3.1.1)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rqrcode_core (2.0.1)
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@ -1009,7 +1009,7 @@ DEPENDENCIES
oj (~> 3.14)
omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-rails_csrf_protection (~> 2.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.7.0)
@ -1036,7 +1036,7 @@ DEPENDENCIES
premailer-rails
prometheus_exporter (~> 2.2)
propshaft
public_suffix (~> 6.0)
public_suffix (~> 7.0)
puma (~> 7.0)
pundit (~> 2.3)
rack-attack (~> 6.6)

View File

@ -1,25 +0,0 @@
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
export function fetchBundleRequest(skipLoading) {
return {
type: BUNDLE_FETCH_REQUEST,
skipLoading,
};
}
export function fetchBundleSuccess(skipLoading) {
return {
type: BUNDLE_FETCH_SUCCESS,
skipLoading,
};
}
export function fetchBundleFail(error, skipLoading) {
return {
type: BUNDLE_FETCH_FAIL,
error,
skipLoading,
};
}

View File

@ -51,7 +51,7 @@ export interface ApiPreviewCardJSON {
html: string;
width: number;
height: number;
image: string;
image: string | null;
image_description: string;
embed_url: string;
blurhash: string;

View File

@ -159,8 +159,8 @@ export default class AutosuggestInput extends ImmutablePureComponent {
this.input.focus();
};
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
componentDidUpdate (prevProps) {
if (prevProps.suggestions !== this.props.suggestions && this.props.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}

View File

@ -242,11 +242,11 @@ class MediaGallery extends PureComponent {
window.removeEventListener('resize', this.handleResize);
}
UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
} else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
this.setState({ visible: nextProps.visible });
componentDidUpdate (prevProps) {
if (!is(prevProps.media, this.props.media) && this.props.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all' });
} else if (!is(prevProps.visible, this.props.visible) && this.props.visible !== undefined) {
this.setState({ visible: this.props.visible });
}
}

View File

@ -61,14 +61,6 @@ class ModalRoot extends PureComponent {
this.history = this.props.history || createBrowserHistory();
}
UNSAFE_componentWillReceiveProps (nextProps) {
if (!!nextProps.children && !this.props.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
}
}
componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));
@ -85,9 +77,15 @@ class ModalRoot extends PureComponent {
this._handleModalClose();
}
if (this.props.children && !prevProps.children) {
this.activeElement = document.activeElement;
this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true));
this._handleModalOpen();
}
if (this.props.children) {
this._ensureHistoryBuffer();
}

View File

@ -538,9 +538,8 @@ class Status extends ImmutablePureComponent {
} else if (status.get('card') && !status.get('quote')) {
media = (
<Card
onOpenMedia={this.handleOpenMedia}
key={`${status.get('id')}-${status.get('edited_at')}`}
card={status.get('card')}
compact
sensitive={status.get('sensitive')}
/>
);

View File

@ -38,7 +38,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchBlocks());
}

View File

@ -61,8 +61,14 @@ class ModifierPickerMenu extends PureComponent {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
};
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.active) {
componentDidMount() {
if (this.props.active) {
this.attachListeners();
}
}
componentDidUpdate() {
if (this.props.active) {
this.attachListeners();
} else {
this.removeListeners();

View File

@ -41,7 +41,7 @@ class Favourites extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
};
UNSAFE_componentWillMount () {
componentDidMount () {
if (!this.props.accountIds) {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
}

View File

@ -45,7 +45,7 @@ class FollowRequests extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchFollowRequests());
}

View File

@ -73,11 +73,10 @@ class ListTimeline extends PureComponent {
this.disconnect = dispatch(connectListStream(id));
}
UNSAFE_componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
const { id } = nextProps.params;
componentDidUpdate (prevProps) {
const { dispatch, params: {id} } = this.props;
if (id !== this.props.params.id) {
if (id !== prevProps.params.id) {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;

View File

@ -40,7 +40,7 @@ class Mutes extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchMutes());
}

View File

@ -34,7 +34,7 @@ class PinnedStatuses extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchPinnedStatuses());
}

View File

@ -42,7 +42,7 @@ class Reblogs extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
};
UNSAFE_componentWillMount () {
componentDidMount () {
if (!this.props.accountIds) {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}

View File

@ -1,254 +0,0 @@
import punycode from 'punycode';
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { is } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useBlurhash } from 'mastodon/initial_state';
const IDNA_PREFIX = 'xn--';
const decodeIDNA = domain => {
return domain
.split('.')
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
.join('.');
};
const getHostname = url => {
const parser = document.createElement('a');
parser.href = url;
return parser.hostname;
};
const domParser = new DOMParser();
const handleIframeUrl = (html, url, providerName) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
const startTime = new URL(url).searchParams.get('t')
if (iframe) {
const iframeUrl = new URL(iframe.src)
iframeUrl.searchParams.set('autoplay', 1)
iframeUrl.searchParams.set('auto_play', 1)
if (startTime && providerName === "YouTube") iframeUrl.searchParams.set('start', startTime)
iframe.src = iframeUrl.href
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
return document.querySelector('body').innerHTML;
}
return html;
};
export default class Card extends PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired,
sensitive: PropTypes.bool,
};
state = {
previewLoaded: false,
embedded: false,
revealed: !this.props.sensitive,
};
UNSAFE_componentWillReceiveProps (nextProps) {
if (!is(this.props.card, nextProps.card)) {
this.setState({ embedded: false, previewLoaded: false });
}
if (this.props.sensitive !== nextProps.sensitive) {
this.setState({ revealed: !nextProps.sensitive });
}
}
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
handleEmbedClick = () => {
this.setState({ embedded: true });
};
handleExternalLinkClick = (e) => {
e.stopPropagation();
};
setRef = c => {
this.node = c;
};
handleImageLoad = () => {
this.setState({ previewLoaded: true });
};
handleReveal = e => {
e.preventDefault();
e.stopPropagation();
this.setState({ revealed: true });
};
renderVideo () {
const { card } = this.props;
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
return (
<div
ref={this.setRef}
className='status-card__image status-card-video'
dangerouslySetInnerHTML={content}
style={{ aspectRatio: '16 / 9' }}
/>
);
}
render () {
const { card } = this.props;
const { embedded, revealed } = this.state;
if (card === null) {
return null;
}
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const interactive = card.get('type') === 'video';
const language = card.get('language') || '';
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
const showAuthor = !!card.getIn(['authors', 0, 'accountId']);
const description = (
<div className='status-card__content' dir='auto'>
<span className='status-card__host'>
<span lang={language}>{provider}</span>
{card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}
</span>
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
{!showAuthor && (card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>)}
</div>
);
const thumbnailStyle = {
visibility: revealed ? null : 'hidden',
};
if (largeImage && card.get('type') === 'video') {
thumbnailStyle.aspectRatio = `16 / 9`;
} else if (largeImage) {
thumbnailStyle.aspectRatio = '1.91 / 1';
} else {
thumbnailStyle.aspectRatio = 1;
}
let embed;
let canvas = (
<Blurhash
className={classNames('status-card__image-preview', {
'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
})}
hash={card.get('blurhash')}
dummy={!useBlurhash}
/>
);
const thumbnailDescription = card.get('image_description');
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>
<FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button>
);
spoilerButton = (
<div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
{spoilerButton}
</div>
);
if (interactive) {
if (embedded) {
embed = this.renderVideo();
} else {
embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}
{revealed ? (
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div>
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
<a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener'><Icon id='external-link' icon={OpenInNewIcon} /></a>
</div>
</div>
) : spoilerButton}
</div>
);
}
return (
<div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
{embed}
<a href={card.get('url')} target='_blank' rel='noopener'>{description}</a>
</div>
);
} else if (card.get('image')) {
embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}
</div>
);
} else {
embed = (
<div className='status-card__image'>
<Icon id='file-text' icon={DescriptionIcon} />
</div>
);
}
return (
<>
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener' ref={this.setRef}>
{embed}
{description}
</a>
{showAuthor && <MoreFromAuthor accountId={card.getIn(['authors', 0, 'accountId'])} />}
</>
);
}
}

View File

@ -0,0 +1,316 @@
import punycode from 'node:punycode';
import { useCallback, useId, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useBlurhash } from 'mastodon/initial_state';
import type { Card as CardType } from 'mastodon/models/status';
const IDNA_PREFIX = 'xn--';
const decodeIDNA = (domain: string) => {
return domain
.split('.')
.map((part) =>
part.startsWith(IDNA_PREFIX)
? punycode.decode(part.slice(IDNA_PREFIX.length))
: part,
)
.join('.');
};
const getHostname = (url: string) => {
const parser = document.createElement('a');
parser.href = url;
return parser.hostname;
};
const domParser = new DOMParser();
const handleIframeUrl = (html: string, url: string, providerName: string) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
const startTime = new URL(url).searchParams.get('t');
if (iframe) {
const iframeUrl = new URL(iframe.src);
iframeUrl.searchParams.set('autoplay', '1');
iframeUrl.searchParams.set('auto_play', '1');
if (startTime && providerName === 'YouTube')
iframeUrl.searchParams.set('start', startTime);
iframe.src = iframeUrl.href;
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
return document.querySelector('body')?.innerHTML ?? '';
}
return html;
};
interface CardProps {
card: CardType | null;
sensitive?: boolean;
}
const CardVideo: React.FC<Pick<CardProps, 'card'>> = ({ card }) => (
<div
className='status-card__image status-card-video'
dangerouslySetInnerHTML={{
__html: card
? handleIframeUrl(
card.get('html'),
card.get('url'),
card.get('provider_name'),
)
: '',
}}
style={{ aspectRatio: '16 / 9' }}
/>
);
const Card: React.FC<CardProps> = ({ card, sensitive }) => {
const [previewLoaded, setPreviewLoaded] = useState(false);
const [embedded, setEmbedded] = useState(false);
const [revealed, setRevealed] = useState(!sensitive);
const handleEmbedClick = useCallback(() => {
setEmbedded(true);
}, []);
const handleExternalLinkClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
}, []);
const handleImageLoad = useCallback(() => {
setPreviewLoaded(true);
}, []);
const handleReveal = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setRevealed(true);
}, []);
const spoilerButtonId = useId();
if (card === null) {
return null;
}
const provider =
card.get('provider_name').length === 0
? decodeIDNA(getHostname(card.get('url')))
: card.get('provider_name');
const interactive = card.get('type') === 'video';
const language = card.get('language') || '';
const hasImage = (card.get('image')?.length ?? 0) > 0;
const largeImage =
(hasImage && card.get('width') > card.get('height')) || interactive;
const showAuthor = !!card.getIn(['authors', 0, 'accountId']);
const description = (
<div className='status-card__content' dir='auto'>
<span className='status-card__host'>
<span lang={language}>{provider}</span>
{card.get('published_at') && (
<>
{' '}
· <RelativeTimestamp timestamp={card.get('published_at')} />
</>
)}
</span>
<strong
className='status-card__title'
title={card.get('title')}
lang={language}
>
{card.get('title')}
</strong>
{!showAuthor &&
(card.get('author_name').length > 0 ? (
<span className='status-card__author'>
<FormattedMessage
id='link_preview.author'
defaultMessage='By {name}'
values={{ name: <strong>{card.get('author_name')}</strong> }}
/>
</span>
) : (
<span className='status-card__description' lang={language}>
{card.get('description')}
</span>
))}
</div>
);
const thumbnailStyle: React.CSSProperties = {
visibility: revealed ? undefined : 'hidden',
aspectRatio: '1',
};
if (largeImage && card.get('type') === 'video') {
thumbnailStyle.aspectRatio = `16 / 9`;
} else if (largeImage) {
thumbnailStyle.aspectRatio = '1.91 / 1';
}
let embed;
const canvas = (
<Blurhash
className={classNames('status-card__image-preview', {
'status-card__image-preview--hidden': revealed && previewLoaded,
})}
hash={card.get('blurhash')}
dummy={!useBlurhash}
/>
);
const thumbnailDescription = card.get('image_description');
const thumbnail = (
<img
src={card.get('image') ?? undefined}
alt={thumbnailDescription}
title={thumbnailDescription}
lang={language}
style={thumbnailStyle}
onLoad={handleImageLoad}
className='status-card__image-image'
/>
);
const spoilerButton = (
<div
className={classNames('spoiler-button', {
'spoiler-button--minified': revealed,
})}
id={spoilerButtonId}
>
<button
type='button'
onClick={handleReveal}
className='spoiler-button__overlay'
>
<span className='spoiler-button__overlay__label'>
<FormattedMessage
id='status.sensitive_warning'
defaultMessage='Sensitive content'
/>
<span className='spoiler-button__overlay__action'>
<FormattedMessage
id='status.media.show'
defaultMessage='Click to show'
/>
</span>
</span>
</button>
</div>
);
if (interactive) {
if (embedded) {
embed = <CardVideo card={card} />;
} else {
embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}
{revealed ? (
<div
className='status-card__actions'
onClick={handleEmbedClick}
role='none'
>
<div>
<button type='button' onClick={handleEmbedClick}>
<Icon id='play' icon={PlayArrowIcon} />
</button>
<a
href={card.get('url')}
onClick={handleExternalLinkClick}
target='_blank'
rel='noopener'
>
<Icon id='external-link' icon={OpenInNewIcon} />
</a>
</div>
</div>
) : (
spoilerButton
)}
</div>
);
}
return (
<div className={classNames('status-card', { expanded: largeImage })}>
{embed}
<a
href={card.get('url')}
target='_blank'
rel='noopener'
onClick={revealed ? undefined : handleReveal}
aria-describedby={revealed ? undefined : spoilerButtonId}
>
{description}
</a>
</div>
);
} else if (card.get('image')) {
embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}
</div>
);
} else {
embed = (
<div className='status-card__image'>
<Icon id='file-text' icon={DescriptionIcon} />
</div>
);
}
return (
<>
<a
href={card.get('url')}
className={classNames('status-card', {
expanded: largeImage,
bottomless: showAuthor,
})}
target='_blank'
rel='noopener'
>
{embed}
{description}
</a>
{showAuthor && (
<MoreFromAuthor
accountId={card.getIn(['authors', 0, 'accountId']) as string}
/>
)}
</>
);
};
// eslint-disable-next-line import/no-default-export
export default Card;

View File

@ -262,8 +262,8 @@ export const DetailedStatus: React.FC<{
} else if (status.get('card') && !status.get('quote')) {
media = (
<Card
key={`${status.get('id')}-${status.get('edited_at')}`}
sensitive={status.get('sensitive')}
onOpenMedia={onOpenMedia}
card={status.get('card')}
/>
);

View File

@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react';
const emptyComponent = () => null;
const noop = () => { };
class Bundle extends PureComponent {
@ -12,18 +11,12 @@ class Bundle extends PureComponent {
error: PropTypes.func,
children: PropTypes.func.isRequired,
renderDelay: PropTypes.number,
onFetch: PropTypes.func,
onFetchSuccess: PropTypes.func,
onFetchFail: PropTypes.func,
};
static defaultProps = {
loading: emptyComponent,
error: emptyComponent,
renderDelay: 0,
onFetch: noop,
onFetchSuccess: noop,
onFetchFail: noop,
};
static cache = new Map;
@ -33,13 +26,13 @@ class Bundle extends PureComponent {
forceRender: false,
};
UNSAFE_componentWillMount() {
componentDidMount() {
this.load(this.props);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.fetchComponent !== this.props.fetchComponent) {
this.load(nextProps);
componentDidUpdate(prevProps) {
if (prevProps.fetchComponent !== this.props.fetchComponent) {
this.load(this.props);
}
}
@ -50,7 +43,7 @@ class Bundle extends PureComponent {
}
load = (props) => {
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
const { fetchComponent, renderDelay } = props || this.props;
const cachedMod = Bundle.cache.get(fetchComponent);
if (fetchComponent === undefined) {
@ -58,11 +51,8 @@ class Bundle extends PureComponent {
return Promise.resolve();
}
onFetch();
if (cachedMod) {
this.setState({ mod: cachedMod.default });
onFetchSuccess();
return Promise.resolve();
}
@ -77,11 +67,9 @@ class Bundle extends PureComponent {
.then((mod) => {
Bundle.cache.set(fetchComponent, mod);
this.setState({ mod: mod.default });
onFetchSuccess();
})
.catch((error) => {
this.setState({ mod: null });
onFetchFail(error);
});
};

View File

@ -5,7 +5,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container';
import {
Compose,
Notifications,
@ -21,6 +20,7 @@ import {
} from '../util/async-components';
import { useColumnsContext } from '../util/columns_context';
import Bundle from './bundle';
import BundleColumnError from './bundle_column_error';
import { ColumnLoading } from './column_loading';
import { ComposePanel, RedirectToMobileComposeIfNeeded } from './compose_panel';
@ -145,9 +145,9 @@ export default class ColumnsArea extends ImmutablePureComponent {
const other = params && params.other ? params.other : {};
return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
<Bundle key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</BundleContainer>
</Bundle>
);
})}

View File

@ -21,11 +21,10 @@ import {
AnnualReportModal,
} from 'mastodon/features/ui/util/async-components';
import BundleContainer from '../containers/bundle_container';
import { ActionsModal } from './actions_modal';
import AudioModal from './audio_modal';
import { BoostModal } from './boost_modal';
import Bundle from './bundle';
import {
ConfirmationModal,
ConfirmDeleteStatusModal,
@ -136,11 +135,11 @@ export default class ModalRoot extends PureComponent {
<Base backgroundColor={backgroundColor} onClose={this.handleClose} ignoreFocus={ignoreFocus}>
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
<Bundle key={type} fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => {
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />;
}}
</BundleContainer>
</Bundle>
<Helmet>
<meta name='robots' content='noindex' />

View File

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
import Bundle from '../components/bundle';
const mapDispatchToProps = dispatch => ({
onFetch () {
dispatch(fetchBundleRequest());
},
onFetchSuccess () {
dispatch(fetchBundleSuccess());
},
onFetchFail (error) {
dispatch(fetchBundleFail(error));
},
});
export default connect(null, mapDispatchToProps)(Bundle);

View File

@ -108,7 +108,7 @@ class SwitchingColumnsArea extends PureComponent {
forceOnboarding: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
}

View File

@ -5,9 +5,9 @@ import { Switch, Route, useLocation } from 'react-router-dom';
import StackTrace from 'stacktrace-js';
import Bundle from '../components/bundle';
import BundleColumnError from '../components/bundle_column_error';
import { ColumnLoading } from '../components/column_loading';
import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components
export const WrappedSwitch = ({ multiColumn, children }) => {
@ -80,9 +80,9 @@ export class WrappedRoute extends Component {
}
return (
<BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
<Bundle fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
{Component => <Component params={match.params} multiColumn={multiColumn} {...componentParams}>{content}</Component>}
</BundleContainer>
</Bundle>
);
};

View File

@ -31,6 +31,8 @@
"account.edit_profile_short": "Edita",
"account.enable_notifications": "Avizame kuando @{name} publike",
"account.endorse": "Avalia en profil",
"account.familiar_followers_one": "Segido por {name1}",
"account.familiar_followers_two": "Segido por {name1} i {name2}",
"account.featured": "Avaliado",
"account.featured.accounts": "Profiles",
"account.featured.hashtags": "Etiketas",
@ -46,6 +48,7 @@
"account.followers": "Suivantes",
"account.followers.empty": "Por agora dingun no sige a este utilizador.",
"account.followers_counter": "{count, plural, one {{counter} suivante} other {{counter} suivantes}}",
"account.followers_you_know_counter": "{counter} ke koneses",
"account.following": "Sigiendo",
"account.following_counter": "{count, plural, other {Sigiendo a {counter}}}",
"account.follows.empty": "Este utilizador ainda no sige a dingun.",
@ -157,6 +160,7 @@
"column.edit_list": "Edita lista",
"column.favourites": "Te plazen",
"column.firehose": "Linyas en bivo",
"column.firehose_singular": "Linya en bivo",
"column.follow_requests": "Solisitudes de segimiento",
"column.home": "Linya prinsipala",
"column.lists": "Listas",
@ -351,6 +355,7 @@
"follow_suggestions.who_to_follow": "A ken segir",
"followed_tags": "Etiketas segidas",
"footer.about": "Sovre mozotros",
"footer.about_this_server": "Sovre mozotros",
"footer.directory": "Katalogo de profiles",
"footer.get_app": "Abasha aplikasyon",
"footer.keyboard_shortcuts": "Akortamientos de klaviatura",
@ -522,6 +527,7 @@
"notification.label.mention": "Enmenta",
"notification.label.private_mention": "Enmentadura privada",
"notification.label.private_reply": "Repuesta privada",
"notification.label.quote": "{name} sito tu publikasyon",
"notification.label.reply": "Arisponde",
"notification.mention": "Enmenta",
"notification.mentioned_you": "{name} te enmento",

View File

@ -173,6 +173,7 @@
"column.edit_list": "Edytuj listę",
"column.favourites": "Ulubione",
"column.firehose": "Aktualności",
"column.firehose_singular": "Na żywo",
"column.follow_requests": "Prośby o obserwację",
"column.home": "Strona główna",
"column.list_members": "Zarządzaj osobami na liście",

View File

@ -45,7 +45,7 @@
"account.follow_request": "Pedir para seguir",
"account.follow_request_cancel": "Cancelar solicitação",
"account.follow_request_cancel_short": "Cancelar",
"account.follow_request_short": "Solicitação",
"account.follow_request_short": "Solicitar",
"account.followers": "Seguidores",
"account.followers.empty": "Nada aqui.",
"account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}",
@ -130,18 +130,18 @@
"annual_report.summary.most_used_hashtag.none": "Nenhuma",
"annual_report.summary.new_posts.new_posts": "novas publicações",
"annual_report.summary.percentile.text": "<topLabel>Isso lhe coloca no topo</topLabel><percentage></percentage><bottomLabel>de usuários de {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Não contaremos à Bernie.",
"annual_report.summary.percentile.we_wont_tell_bernie": "Não contaremos ao Bernie.",
"annual_report.summary.thanks": "Obrigada por fazer parte do Mastodon!",
"attachments_list.unprocessed": "(não processado)",
"audio.hide": "Ocultar áudio",
"block_modal.remote_users_caveat": "Pediremos ao servidor {domain} que respeite sua decisão. No entanto, a conformidade não é garantida, já que alguns servidores podem lidar com bloqueios de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
"block_modal.remote_users_caveat": "Pediremos ao servidor {domain} que respeite sua decisão. No entanto, a conformidade não é garantida, já que alguns servidores podem lidar com bloqueios de maneira diferente. As publicações públicas ainda podem estar visíveis para usuários não logados.",
"block_modal.show_less": "Mostrar menos",
"block_modal.show_more": "Mostrar mais",
"block_modal.they_cant_mention": "Eles não podem mencionar ou seguir você.",
"block_modal.they_cant_see_posts": "Eles não podem ver suas postagens e você não verá as deles.",
"block_modal.they_will_know": "Eles podem ver que estão bloqueados.",
"block_modal.they_cant_mention": "Não poderá mencionar ou seguir você.",
"block_modal.they_cant_see_posts": "Não poderá ver suas publicações e você não verá as dele/a.",
"block_modal.they_will_know": "Poderá ver que você bloqueou.",
"block_modal.title": "Bloquear usuário?",
"block_modal.you_wont_see_mentions": "Você não verá publicações que os mencionem.",
"block_modal.you_wont_see_mentions": "Você não verá publicações que mencionem o usuário.",
"boost_modal.combo": "Pressione {combo} para pular isso na próxima vez",
"boost_modal.reblog": "Impulsionar a publicação?",
"boost_modal.undo_reblog": "Retirar o impulsionamento do post?",
@ -157,8 +157,8 @@
"bundle_modal_error.close": "Fechar",
"bundle_modal_error.message": "Algo deu errado ao carregar esta tela.",
"bundle_modal_error.retry": "Tente novamente",
"carousel.current": "<sr>Deslize</sr> {current, number} / {max, number}",
"carousel.slide": "Deslize {current, number} of {max, number}",
"carousel.current": "<sr>Slide</sr> {current, number} / {max, number}",
"carousel.slide": "Slide {current, number} of {max, number}",
"closed_registrations.other_server_instructions": "Como o Mastodon é descentralizado, você pode criar uma conta em outro servidor e ainda pode interagir com este.",
"closed_registrations_modal.description": "Não é possível criar uma conta em {domain} no momento, mas atente que você não precisa de uma conta especificamente em {domain} para usar o Mastodon.",
"closed_registrations_modal.find_another_server": "Encontrar outro servidor",
@ -196,12 +196,12 @@
"community.column_settings.local_only": "Somente local",
"community.column_settings.media_only": "Somente mídia",
"community.column_settings.remote_only": "Somente global",
"compose.error.blank_post": "A postagem não pode estar em branco.",
"compose.error.blank_post": "A publicação não pode estar em branco.",
"compose.language.change": "Alterar idioma",
"compose.language.search": "Pesquisar idiomas...",
"compose.published.body": "Publicado.",
"compose.published.open": "Abrir",
"compose.saved.body": "Postagem salva.",
"compose.saved.body": "Publicação salva.",
"compose_form.direct_message_warning_learn_more": "Saiba mais",
"compose_form.encryption_warning": "As publicações no Mastodon não são criptografadas de ponta-a-ponta. Não compartilhe nenhuma informação sensível no Mastodon.",
"compose_form.hashtag_warning": "Esta publicação não será exibida sob nenhuma hashtag, já que não é pública. Apenas postagens públicas podem ser pesquisadas por meio de hashtags.",
@ -217,7 +217,7 @@
"compose_form.poll.type": "Estilo",
"compose_form.publish": "Publicar",
"compose_form.reply": "Responder",
"compose_form.save_changes": "Atualização",
"compose_form.save_changes": "Atualizar",
"compose_form.spoiler.marked": "Com Aviso de Conteúdo",
"compose_form.spoiler.unmarked": "Sem Aviso de Conteúdo",
"compose_form.spoiler_placeholder": "Aviso de conteúdo (opcional)",
@ -231,11 +231,11 @@
"confirmations.delete_list.title": "Excluir lista?",
"confirmations.discard_draft.confirm": "Descartar e continuar",
"confirmations.discard_draft.edit.cancel": "Continuar editando",
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas ao post sendo editado.",
"confirmations.discard_draft.edit.title": "Descartar mudanças no seu post?",
"confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas à publicação sendo editada.",
"confirmations.discard_draft.edit.title": "Descartar mudanças na sua publicação?",
"confirmations.discard_draft.post.cancel": "Continuar rascunho",
"confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.",
"confirmations.discard_draft.post.title": "Eliminar seu esboço de publicação?",
"confirmations.discard_draft.post.title": "Eliminar seu rascunho de publicação?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Há mudanças não salvas na descrição ou pré-visualização da mídia. Descartar assim mesmo?",
"confirmations.follow_to_list.confirm": "Seguir e adicionar à lista",
@ -246,13 +246,13 @@
"confirmations.logout.title": "Sair da sessão?",
"confirmations.missing_alt_text.confirm": "Adicione texto alternativo",
"confirmations.missing_alt_text.message": "Seu post contém mídia sem texto alternativo. Adicionar descrições ajuda a tornar seu conteúdo acessível para mais pessoas.",
"confirmations.missing_alt_text.secondary": "Postar mesmo assim",
"confirmations.missing_alt_text.secondary": "Publicar mesmo assim",
"confirmations.missing_alt_text.title": "Adicionar texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.private_quote_notify.cancel": "Voltar à edição",
"confirmations.private_quote_notify.confirm": "Publicar postagem",
"confirmations.private_quote_notify.confirm": "Publicar citação",
"confirmations.private_quote_notify.do_not_show_again": "Não me mostre esta mensagem novamente",
"confirmations.private_quote_notify.message": "A pessoa que está citando e outras menções serão notificadas e poderão ver sua postagem, mesmo que não sigam você.",
"confirmations.private_quote_notify.message": "A pessoa que está sendo citada e outras mencionadas serão notificadas e poderão ver sua publicação, mesmo que não sigam você.",
"confirmations.private_quote_notify.title": "Compartilhar com seguidores e usuários mencionados?",
"confirmations.quiet_post_quote_info.dismiss": "Não me lembrar novamente",
"confirmations.quiet_post_quote_info.got_it": "Entendi",
@ -265,14 +265,14 @@
"confirmations.remove_from_followers.message": "{name} vai parar de te seguir. Tem certeza de que deseja continuar?",
"confirmations.remove_from_followers.title": "Remover seguidor?",
"confirmations.revoke_quote.confirm": "Remover publicação",
"confirmations.revoke_quote.message": "Essa ação não pode ser desfeita.",
"confirmations.revoke_quote.message": "Esta ação não pode ser desfeita.",
"confirmations.revoke_quote.title": "Remover publicação?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "Desbloquear {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.title": "Deixar de seguir {name}?",
"confirmations.withdraw_request.confirm": "Retirar solicitação",
"confirmations.withdraw_request.title": "Cancelar solicitação para seguir {name}?",
"confirmations.withdraw_request.title": "Retirar solicitação para seguir {name}?",
"content_warning.hide": "Ocultar publicação",
"content_warning.show": "Mostrar mesmo assim",
"content_warning.show_more": "Mostrar mais",
@ -369,9 +369,9 @@
"explore.trending_links": "Notícias",
"explore.trending_statuses": "Publicações",
"explore.trending_tags": "Hashtags",
"featured_carousel.current": "<sr>Postar</sr> {current, number} / {max, number}",
"featured_carousel.current": "<sr>Publicação</sr> {current, number} / {max, number}",
"featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}",
"featured_carousel.slide": "Postar {current, number} of {max, number}",
"featured_carousel.slide": "Publicação {current, number} of {max, number}",
"filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.",
"filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!",
"filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.",

View File

@ -5,7 +5,7 @@
"about.disclaimer": "Mastodon 是一個自由的開源軟體,是 Mastodon gGmbH 之註冊商標。",
"about.domain_blocks.no_reason_available": "無法存取的原因",
"about.domain_blocks.preamble": "Mastodon 基本上允許您瀏覽聯邦宇宙中任何伺服器的內容並與使用者互動。以下是於本伺服器上設定之例外。",
"about.domain_blocks.silenced.explanation": "一般來說您不會看到來自這個伺服器的個人檔案與內容,除非您明確地打開或著跟隨此個人檔案。",
"about.domain_blocks.silenced.explanation": "一般來說您不會看到來自這個伺服器的個人檔案與內容,除非您明確地檢視或著跟隨此個人檔案。",
"about.domain_blocks.silenced.title": "已受限",
"about.domain_blocks.suspended.explanation": "來自此伺服器的資料都不會被處理、儲存或交換,也無法和此伺服器上的使用者互動與交流。",
"about.domain_blocks.suspended.title": "已停權",
@ -90,7 +90,7 @@
"account.unmute": "解除靜音 @{name}",
"account.unmute_notifications_short": "解除靜音推播通知",
"account.unmute_short": "解除靜音",
"account_note.placeholder": "按此新增備註",
"account_note.placeholder": "點擊以新增備註",
"admin.dashboard.daily_retention": "註冊後使用者存留率(日)",
"admin.dashboard.monthly_retention": "註冊後使用者存留率(月)",
"admin.dashboard.retention.average": "平均",
@ -138,10 +138,10 @@
"block_modal.show_less": "減少顯示",
"block_modal.show_more": "顯示更多",
"block_modal.they_cant_mention": "他們無法提及或跟隨您。",
"block_modal.they_cant_see_posts": "他們無法讀取您的嘟文,且您不會見到他們。",
"block_modal.they_cant_see_posts": "他們無法讀取您的嘟文,且您不會見到他們的嘟文。",
"block_modal.they_will_know": "他們能見到他們已被封鎖。",
"block_modal.title": "是否封鎖該使用者?",
"block_modal.you_wont_see_mentions": "您不會見到提及他們的嘟文。",
"block_modal.you_wont_see_mentions": "您不會見到提及他們的嘟文。",
"boost_modal.combo": "您下次可以按 {combo} 跳過",
"boost_modal.reblog": "是否要轉嘟?",
"boost_modal.undo_reblog": "是否要取消轉嘟?",
@ -181,7 +181,7 @@
"column.home": "首頁",
"column.list_members": "管理列表成員",
"column.lists": "列表",
"column.mutes": "已靜音使用者",
"column.mutes": "已靜音使用者",
"column.notifications": "推播通知",
"column.pins": "釘選的嘟文",
"column.public": "聯邦時間軸",
@ -193,9 +193,9 @@
"column_header.show_settings": "顯示設定",
"column_header.unpin": "取消釘選",
"column_search.cancel": "取消",
"community.column_settings.local_only": "顯示本站",
"community.column_settings.media_only": "顯示媒體",
"community.column_settings.remote_only": "顯示遠端",
"community.column_settings.local_only": "顯示本站",
"community.column_settings.media_only": "顯示媒體",
"community.column_settings.remote_only": "顯示遠端",
"compose.error.blank_post": "嘟文無法為空白。",
"compose.language.change": "變更語言",
"compose.language.search": "搜尋語言...",
@ -204,14 +204,14 @@
"compose.saved.body": "已儲存嘟文。",
"compose_form.direct_message_warning_learn_more": "了解更多",
"compose_form.encryption_warning": "Mastodon 上的嘟文並未進行端到端加密。請不要透過 Mastodon 分享任何敏感資訊。",
"compose_form.hashtag_warning": "由於這則嘟文設定為非公開,將不會列於任何主題標籤下。只有公開的嘟文才能藉由主題標籤被找到。",
"compose_form.lock_disclaimer": "您的帳號尚未 {locked}。任何人皆能跟隨您並看到您設定成只對跟隨者顯示的嘟文。",
"compose_form.hashtag_warning": "由於這則嘟文設定為「不公開」,它將不被列於任何主題標籤下。只有公開的嘟文才能藉由主題標籤被找到。",
"compose_form.lock_disclaimer": "您的帳號尚未 {locked}。任何人皆能跟隨您並看到您設定成僅有跟隨者可見的嘟文。",
"compose_form.lock_disclaimer.lock": "上鎖",
"compose_form.placeholder": "正在想些什麼嗎?",
"compose_form.poll.duration": "投票期限",
"compose_form.poll.multiple": "多選",
"compose_form.poll.option_placeholder": "選項 {number}",
"compose_form.poll.single": "單",
"compose_form.poll.single": "單選",
"compose_form.poll.switch_to_multiple": "變更投票為允許多個選項",
"compose_form.poll.switch_to_single": "變更投票為允許單一選項",
"compose_form.poll.type": "投票方式",
@ -231,7 +231,7 @@
"confirmations.delete_list.title": "是否刪除該列表?",
"confirmations.discard_draft.confirm": "捨棄並繼續",
"confirmations.discard_draft.edit.cancel": "恢復編輯",
"confirmations.discard_draft.edit.message": "繼續將捨棄任何您對正在編輯的此嘟文進行之任何變更。",
"confirmations.discard_draft.edit.message": "繼續將捨棄任何您對正在編輯的此嘟文進行之任何變更。",
"confirmations.discard_draft.edit.title": "是否捨棄對您的嘟文之變更?",
"confirmations.discard_draft.post.cancel": "恢復草稿",
"confirmations.discard_draft.post.message": "繼續將捨棄您正在撰寫中之嘟文。",
@ -259,20 +259,20 @@
"confirmations.quiet_post_quote_info.message": "當引用不於公開時間軸顯示之嘟文時,您的嘟文將自熱門時間軸隱藏。",
"confirmations.quiet_post_quote_info.title": "引用不於公開時間軸顯示之嘟文",
"confirmations.redraft.confirm": "刪除並重新編輯",
"confirmations.redraft.message": "您確定要刪除這則嘟文並重新編輯嗎?您將失去這則嘟文之轉嘟及最愛,且對此嘟文之回覆會變成獨立的嘟文。",
"confirmations.redraft.message": "您確定要刪除這則嘟文並重新編輯嗎?您將失去此嘟文之轉嘟及最愛,且對原嘟文之回覆將變成獨立嘟文。",
"confirmations.redraft.title": "是否刪除並重新編輯該嘟文?",
"confirmations.remove_from_followers.confirm": "移除跟隨者",
"confirmations.remove_from_followers.message": "{name} 將停止跟隨您。您確定要繼續嗎?",
"confirmations.remove_from_followers.message": "{name} 將停止跟隨您。您確定要繼續嗎?",
"confirmations.remove_from_followers.title": "是否移除該跟隨者?",
"confirmations.revoke_quote.confirm": "移除嘟文",
"confirmations.revoke_quote.message": "此動作無法復原。",
"confirmations.revoke_quote.title": "是否確定移除嘟文?",
"confirmations.revoke_quote.title": "是否移除嘟文?",
"confirmations.unblock.confirm": "解除封鎖",
"confirmations.unblock.title": "解除封鎖 {name}",
"confirmations.unblock.title": "是否解除封鎖 {name}",
"confirmations.unfollow.confirm": "取消跟隨",
"confirmations.unfollow.title": "取消跟隨 {name}",
"confirmations.unfollow.title": "是否取消跟隨 {name}",
"confirmations.withdraw_request.confirm": "收回跟隨請求",
"confirmations.withdraw_request.title": "收回對 {name} 之跟隨請求?",
"confirmations.withdraw_request.title": "是否收回對 {name} 之跟隨請求?",
"content_warning.hide": "隱藏嘟文",
"content_warning.show": "仍要顯示",
"content_warning.show_more": "顯示更多",
@ -284,7 +284,7 @@
"copypaste.copied": "已複製",
"copypaste.copy_to_clipboard": "複製到剪貼簿",
"directory.federated": "來自已知聯邦宇宙",
"directory.local": "僅來自 {domain} 網域",
"directory.local": "僅來自 {domain}",
"directory.new_arrivals": "新人",
"directory.recently_active": "最近活躍",
"disabled_account_banner.account_settings": "帳號設定",
@ -298,9 +298,9 @@
"domain_block_modal.they_cant_follow": "來自此伺服器之使用者將無法跟隨您。",
"domain_block_modal.they_wont_know": "他們不會知道他們已被封鎖。",
"domain_block_modal.title": "是否封鎖該網域?",
"domain_block_modal.you_will_lose_num_followers": "您將失去 {followersCount, plural, other {{followersCountDisplay} 個跟隨者}} 與 {followingCount, plural, other {{followingCountDisplay} 個您跟隨之帳號}}.",
"domain_block_modal.you_will_lose_num_followers": "您將失去 {followersCount, plural, other {{followersCountDisplay} 個跟隨者}} 與 {followingCount, plural, other {{followingCountDisplay} 個您跟隨之帳號}}",
"domain_block_modal.you_will_lose_relationships": "您將失去所有的跟隨者與您自此伺服器跟隨之帳號。",
"domain_block_modal.you_wont_see_posts": "您不會見到來自此伺服器使用者之任何嘟文或推播通知。",
"domain_block_modal.you_wont_see_posts": "您不會見到來自此伺服器使用者之任何嘟文或推播通知。",
"domain_pill.activitypub_lets_connect": "它使您能於 Mastodon 及其他不同的社群應用程式與人連結及互動。",
"domain_pill.activitypub_like_language": "ActivityPub 像是 Mastodon 與其他社群網路溝通時所用的語言。",
"domain_pill.server": "伺服器",
@ -567,8 +567,8 @@
"mute_modal.they_can_mention_and_follow": "他們仍可提及或跟隨您,但您不會見到他們。",
"mute_modal.they_wont_know": "他們不會知道他們已被靜音。",
"mute_modal.title": "是否靜音該使用者?",
"mute_modal.you_wont_see_mentions": "您不會見到提及他們的嘟文。",
"mute_modal.you_wont_see_posts": "他們仍可讀取您的嘟文,但您不會見到他們的。",
"mute_modal.you_wont_see_mentions": "您不會見到提及他們的嘟文。",
"mute_modal.you_wont_see_posts": "他們仍可讀取您的嘟文,但您不會見到他們的嘟文。",
"navigation_bar.about": "關於",
"navigation_bar.account_settings": "密碼與安全性",
"navigation_bar.administration": "管理介面",
@ -631,7 +631,7 @@
"notification.moderation_warning.action_disable": "您的帳號已被停用。",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "某些您的嘟文已被標記為敏感內容。",
"notification.moderation_warning.action_none": "您的帳號已收到管理員警告。",
"notification.moderation_warning.action_sensitive": "即日起,您的嘟文將被標記為敏感內容。",
"notification.moderation_warning.action_sensitive": "即日起,您的嘟文將被標記為敏感內容。",
"notification.moderation_warning.action_silence": "您的帳號已被限制。",
"notification.moderation_warning.action_suspend": "您的帳號已被停權。",
"notification.own_poll": "您的投票已結束",
@ -649,10 +649,10 @@
"notification_requests.accept": "接受",
"notification_requests.accept_multiple": "{count, plural, other {接受 # 則請求...}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, other {接受請求}}",
"notification_requests.confirm_accept_multiple.message": "您將接受 {count, plural, other {# 則推播通知請求}}。您確定要繼續",
"notification_requests.confirm_accept_multiple.message": "您將接受 {count, plural, other {# 則推播通知請求}}。您確定要繼續",
"notification_requests.confirm_accept_multiple.title": "是否接受推播通知請求?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, other {忽略請求}}",
"notification_requests.confirm_dismiss_multiple.message": "您將忽略 {count, plural, other {# 則推播通知請求}}。您將不再能輕易存取{count, plural, other {這些}}推播通知。您確定要繼續",
"notification_requests.confirm_dismiss_multiple.message": "您將忽略 {count, plural, other {# 則推播通知請求}}。您將不再能輕易存取{count, plural, other {這些}}推播通知。您確定要繼續",
"notification_requests.confirm_dismiss_multiple.title": "是否忽略推播通知請求?",
"notification_requests.dismiss": "關閉",
"notification_requests.dismiss_multiple": "{count, plural, other {忽略 # 則請求...}}",
@ -759,7 +759,7 @@
"privacy.quote.anyone": "{visibility},任何人皆可引用",
"privacy.quote.disabled": "{visibility},停用引用嘟文",
"privacy.quote.limited": "{visibility},受限的引用嘟文",
"privacy.unlisted.additional": "此與公開嘟文完全相同,但嘟文不會出現於即時內容或主題標籤、探索、及 Mastodon 搜尋中,即使您帳戶設定中選擇加入。",
"privacy.unlisted.additional": "此與公開嘟文完全相同,但嘟文不會出現於即時內容或主題標籤、探索、及 Mastodon 搜尋中,即使您帳戶設定中選擇加入。",
"privacy.unlisted.long": "不顯示於 Mastodon 之搜尋結果、熱門趨勢、及公開時間軸上",
"privacy.unlisted.short": "不公開",
"privacy_policy.last_updated": "最後更新:{date}",
@ -792,7 +792,7 @@
"reply_indicator.cancel": "取消",
"reply_indicator.poll": "投票",
"report.block": "封鎖",
"report.block_explanation": "您將不再看到他們的嘟文。他們將無法看到您的嘟文或是跟隨您。他們會發現他們已被封鎖。",
"report.block_explanation": "您將不再看到他們的嘟文。他們將無法檢視您的嘟文或是跟隨您。他們會發現他們已被封鎖。",
"report.categories.legal": "合法性",
"report.categories.other": "其他",
"report.categories.spam": "垃圾訊息",
@ -806,7 +806,7 @@
"report.forward": "轉寄到 {target}",
"report.forward_hint": "這個帳號屬於其他伺服器。要向該伺服器發送匿名的檢舉訊息嗎?",
"report.mute": "靜音",
"report.mute_explanation": "您將不再看到他們的嘟文。他們仍能可以跟隨您以及察看您的嘟文,並且不會知道他們已被靜音。",
"report.mute_explanation": "您將不再看到他們的嘟文。他們仍能可以跟隨您以及檢視您的嘟文,並且不會知道他們已被靜音。",
"report.next": "繼續",
"report.placeholder": "其他備註",
"report.reasons.dislike": "我不喜歡",
@ -996,7 +996,7 @@
"upload_error.limit": "已達到檔案上傳限制。",
"upload_error.poll": "不允許於投票時上傳檔案。",
"upload_error.quote": "引用嘟文無法上傳檔案。",
"upload_form.drag_and_drop.instructions": "請按空白鍵或 Enter 鍵取多媒體附加檔案。使用方向鍵移動多媒體附加檔案。按下空白鍵或 Enter 鍵於新位置放置多媒體附加檔案,或按下 ESC 鍵取消。",
"upload_form.drag_and_drop.instructions": "請按空白鍵或 Enter 鍵取多媒體附加檔案。使用方向鍵移動多媒體附加檔案。按下空白鍵或 Enter 鍵於新位置放置多媒體附加檔案,或按下 ESC 鍵取消。",
"upload_form.drag_and_drop.on_drag_cancel": "移動已取消。多媒體附加檔案 {item} 已被放置。",
"upload_form.drag_and_drop.on_drag_end": "多媒體附加檔案 {item} 已被放置。",
"upload_form.drag_and_drop.on_drag_over": "多媒體附加檔案 {item} 已被移動。",

View File

@ -7699,6 +7699,8 @@ a.status-card {
position: relative;
background: $base-shadow-color;
max-width: 100%;
max-height: max(400px, 60vh);
margin-inline: auto;
border-radius: 8px;
box-sizing: border-box;
color: $white;

View File

@ -122,7 +122,11 @@ $content-width: 840px;
align-items: center;
gap: 6px;
padding: 15px;
color: var(--color-text-secondary);
color: color-mix(
in oklab,
var(--color-text-primary),
var(--color-text-secondary)
);
text-decoration: none;
transition: all 200ms linear;
transition-property: color, background-color;

View File

@ -4494,7 +4494,7 @@ a.status-card {
z-index: 1;
background: radial-gradient(
ellipse,
rgb(from var(--color-bg-brand-softer-base) r g b / 23%) 0%,
rgb(from var(--color-bg-brand-base) r g b / 23%) 0%,
transparent 60%
);
}
@ -7492,6 +7492,8 @@ a.status-card {
color: var(--color-text-on-media);
background: var(--color-bg-media);
max-width: 100%;
max-height: max(400px, 60vh);
margin-inline: auto;
border-radius: 8px;
box-sizing: border-box;
display: flex;

View File

@ -79,7 +79,7 @@
// Utility
--color-bg-ambient: var(--color-bg-primary);
--color-bg-elevated: var(--color-grey-800);
--color-bg-elevated: var(--color-bg-primary);
--color-bg-inverted: var(--color-grey-50);
--color-bg-media-base: var(--color-black);
--color-bg-media-strength: 65%;
@ -87,7 +87,7 @@
var(--color-bg-media-base),
var(--color-bg-media-strength)
)};
--color-bg-overlay: var(--color-bg-primary);
--color-bg-overlay: var(--color-black);
--color-bg-disabled: var(--color-grey-700);
// Brand

View File

@ -27,6 +27,9 @@ class Collection < ApplicationRecord
validates :name, presence: true
validates :description, presence: true
validates :local, inclusion: [true, false]
validates :sensitive, inclusion: [true, false]
validates :discoverable, inclusion: [true, false]
validates :uri, presence: true, if: :remote?
validates :original_number_of_items,
presence: true,

View File

@ -38,9 +38,7 @@ class SessionActivation < ApplicationRecord
end
def activate(**)
activation = create!(**)
purge_old
activation
create!(**).tap { purge_old }
end
def deactivate(id)

View File

@ -63,7 +63,7 @@ class BackupService < BaseService
dump_actor!(zipfile)
end
archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.zip"
archive_filename = "#{['archive', Time.current.to_fs(:number), SecureRandom.hex(16)].join('-')}.zip"
@backup.dump = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
@backup.processed = true

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class CreateCollectionService
def call(params, account)
tag = params.delete(:tag)
account_ids = params.delete(:account_ids)
@collection = Collection.new(params.merge({ account:, local: true, tag: find_or_create_tag(tag) }))
build_items(account_ids)
@collection.save!
@collection
end
private
def find_or_create_tag(name)
return nil if name.blank?
Tag.find_or_create_by_names(name).first
end
def build_items(account_ids)
return if account_ids.blank?
account_ids.each do |account_id|
account = Account.find(account_id)
# TODO: validate preferences
@collection.collection_items.build(account:)
end
end
end

View File

@ -30,6 +30,10 @@ lad:
attributes:
url:
invalid: no es adreso URL valido
collection:
attributes:
tag:
unusable: no se pueden uzar
doorkeeper/application:
attributes:
website:

View File

@ -1593,13 +1593,13 @@ da:
invalid: Denne invitation er ikke gyldig
invited_by: 'Du blev inviteret af:'
max_uses:
one: 1 benyttelse
other: "%{count} benyttelser"
one: 1 anvendelse
other: "%{count} anvendelser"
max_uses_prompt: Ubegrænset
prompt: Generér og del links med andre for at give dem adgang til denne server
table:
expires_at: Udløber
uses: Benyttelser
uses: Anvendelser
title: Invitér personer
link_preview:
author_html: Af %{name}
@ -2122,7 +2122,7 @@ da:
feature_audience_title: Opbyg et publikum i tillid
feature_control: Man ved selv bedst, hvad man ønsker at se på sit hjemmefeed. Ingen algoritmer eller annoncer til at spilde tiden. Følg alle på tværs af enhver Mastodon-server fra en enkelt konto og modtag deres indlæg i kronologisk rækkefølge, og gør dette hjørne af internet lidt mere personligt.
feature_control_title: Hold styr på egen tidslinje
feature_creativity: Mastodon understøtter indlæg med lyd, video og billede, tilgængelighedsbeskrivelser, meningsmålinger, indhold advarsler, animerede avatarer, tilpassede emojis, miniaturebeskæringskontrol og mere, for at gøre det lettere at udtrykke sig online. Uanset om man udgiver sin kunst, musik eller podcast, så står Mastodon til rådighed.
feature_creativity: Mastodon understøtter lyd-, video- og billedindlæg, tilgængelighedsbeskrivelser, afstemninger, indholdsadvarsler, animerede avatarer, brugerdefinerede emojis, kontrol over beskæring af miniaturebilleder og meget mere, så du lettere kan udtrykke dig online. Uanset om du udgiver din kunst, din musik eller din podcast, er Mastodon der for dig.
feature_creativity_title: Uovertruffen kreativitet
feature_moderation: Mastodon lægger beslutningstagning tilbage i brugerens hænder. Hver server opretter deres egne regler og reguleringer, som håndhæves lokalt og ikke ovenfra som virksomhedernes sociale medier, hvilket gør det til den mest fleksible mht. at reagere på behovene hos forskellige persongrupper. Deltag på en server med de regler, man er enige med, eller driv en egen server.
feature_moderation_title: Moderering af måden, tingene bør være på

View File

@ -7,6 +7,7 @@ lad:
send_paranoid_instructions: Si tu adreso de posta elektronika existe en muestra baza de datos, risiviras una posta elektronika kon instruksyones sobre komo konfirmar tu adreso de posta elektronika en pokos minutos.
failure:
already_authenticated: Ya te konektates kon tu kuento.
closed_registrations: Tu prova de enrejistrarte tiene sido blokada por a una politika de red. Si piensas ke esto es un yerro, eskrive a %{email}.
inactive: Tu kuento ainda no tyene sido aktivado.
invalid: "%{authentication_keys} o kod invalido."
last_attempt: Aprova una vez mas antes de ke tu kuento sea blokado.

View File

@ -477,6 +477,7 @@ lad:
created_at: Kriyado en
delete: Efasa
ip: Adreso IP
request_body: Kuerpo de la solisitud
providers:
active: Aktivo
base_url: URL baza

View File

@ -227,7 +227,7 @@ da:
inbox_url: URL til videreformidlingsindbakken
irreversible: Fjern istedet for skjul
locale: Grænsefladesprog
max_uses: Maks. antal afbenyttelser
max_uses: Maks. antal anvendelser
new_password: Ny adgangskode
note: Biografi
otp_attempt: Tofaktorkode

View File

@ -313,7 +313,9 @@ lad:
domain: Domeno
min_age: Edad minima
user:
date_of_birth_1i: Anyo
date_of_birth_2i: Mez
date_of_birth_3i: Diya
role: Rolo
time_zone: Zona de tiempo
user_role:

View File

@ -65,7 +65,7 @@ pt-BR:
setting_display_media_hide_all: Sempre ocultar todas as mídias
setting_display_media_show_all: Sempre mostrar mídia sensível
setting_emoji_style: Como exibir emojis. "Automáticos" tentará usar emojis nativos, mas voltará para o Twemoji para navegadores legados.
setting_quick_boosting_html: Quando ativado, clicar no ícone de impulsionamento %{boost_icon} impulsionará imediatamente o texto, em vez de abrir o menu suspenso de impulsionamento/cotação. Move a ação de cotação para o menu %{options_icon} (Opções).
setting_quick_boosting_html: Quando ativado, clicar no ícone de impulsionamento %{boost_icon} impulsionará imediatamente o texto, em vez de abrir o menu suspenso de impulsionamento/citação. Move a ação de citação para o menu %{options_icon} (Opções).
setting_system_scrollbars_ui: Se aplica apenas para navegadores de computador baseado no Safari e Chrome
setting_use_blurhash: O blur é baseado nas cores da imagem oculta, ofusca a maioria dos detalhes
setting_use_pending_items: Ocultar atualizações da linha do tempo atrás de um clique ao invés de rolar automaticamente
@ -237,7 +237,7 @@ pt-BR:
setting_aggregate_reblogs: Agrupar boosts nas linhas
setting_always_send_emails: Sempre enviar notificações por e-mail
setting_auto_play_gif: Reproduzir GIFs automaticamente
setting_boost_modal: Controle que aumenta a visibilidade
setting_boost_modal: Controlar a visibilidade dos impulsos
setting_default_language: Idioma dos toots
setting_default_privacy: Visibilidade da publicação
setting_default_quote_policy: Quem pode citar

View File

@ -232,8 +232,10 @@ sv:
setting_always_send_emails: Skicka alltid e-postnotiser
setting_auto_play_gif: Spela upp GIF:ar automatiskt
setting_default_language: Inläggsspråk
setting_default_privacy: Inläggssynlighet
setting_default_quote_policy: Vem kan citera
setting_default_sensitive: Markera alltid media som känsligt
setting_delete_modal: Varna mig innan ett inlägg tas bort
setting_disable_hover_cards: Inaktivera profilförhandsgranskning vid hovring
setting_disable_swiping: Inaktivera svepande rörelser
setting_display_media: Mediavisning
@ -243,6 +245,8 @@ sv:
setting_emoji_style: Emoji-stil
setting_expand_spoilers: Utöka alltid tutningar markerade med innehållsvarningar
setting_hide_network: Göm ditt nätverk
setting_missing_alt_text_modal: Varna mig innan jag gör mediainlägg utan alt-text
setting_quick_boosting: Aktivera snabb-boostande
setting_reduce_motion: Minska rörelser i animationer
setting_system_font_ui: Använd systemets standardfont
setting_system_scrollbars_ui: Använd systemets standardrullningsfält
@ -276,12 +280,17 @@ sv:
content_cache_retention_period: Förvaringsperiod för fjärrinnehåll
custom_css: Anpassad CSS
favicon: Favicon
landing_page: Startsida för nya besökare
local_live_feed_access: Åtkomst till live-flöden med lokala inlägg
local_topic_feed_access: Åtkomst till hashtagg och länkflöden med lokala inlägg
mascot: Anpassad maskot (tekniskt arv)
media_cache_retention_period: Tid för bibehållande av mediecache
min_age: Åldersgräns
peers_api_enabled: Publicera lista över upptäckta servrar i API:et
profile_directory: Aktivera profilkatalog
registrations_mode: Vem kan registrera sig
remote_live_feed_access: Åtkomst till live-flöden med fjärrinlägg
remote_topic_feed_access: Åtkomst till hashtagg och länkflöden med fjärrinlägg
require_invite_text: Kräv anledning för att gå med
show_domain_blocks: Visa domänblockeringar
show_domain_blocks_rationale: Visa varför domäner blockerades

View File

@ -62,6 +62,8 @@ module Mastodon::CLI
AND NOT EXISTS (SELECT 1 FROM mentions WHERE statuses.id = mentions.status_id AND mentions.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
AND NOT EXISTS (SELECT 1 FROM favourites WHERE statuses.id = favourites.status_id AND favourites.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
AND NOT EXISTS (SELECT 1 FROM bookmarks WHERE statuses.id = bookmarks.status_id AND bookmarks.account_id IN (SELECT accounts.id FROM accounts WHERE domain IS NULL))
AND NOT EXISTS (SELECT 1 FROM quotes JOIN statuses statuses1 ON quotes.status_id = statuses1.id WHERE quotes.quoted_status_id = statuses.id AND (statuses1.uri IS NULL OR statuses1.local))
AND NOT EXISTS (SELECT 1 FROM quotes JOIN statuses statuses1 ON quotes.quoted_status_id = statuses1.id WHERE quotes.status_id = statuses.id AND (statuses1.uri IS NULL OR statuses1.local))
#{clean_followed_sql}
SQL

View File

@ -1,7 +1,7 @@
{
"name": "@mastodon/mastodon",
"license": "AGPL-3.0-or-later",
"packageManager": "yarn@4.10.3",
"packageManager": "yarn@4.12.0",
"engines": {
"node": ">=20"
},

View File

@ -10,6 +10,12 @@ RSpec.describe Collection do
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to_not allow_value(nil).for(:local) }
it { is_expected.to_not allow_value(nil).for(:sensitive) }
it { is_expected.to_not allow_value(nil).for(:discoverable) }
context 'when collection is remote' do
subject { Fabricate.build :collection, local: false }

View File

@ -39,20 +39,24 @@ RSpec.describe SessionActivation do
end
describe '.activate' do
let(:options) { { user: Fabricate(:user), session_id: '1' } }
let(:user) { Fabricate :user }
let!(:session_activation) { Fabricate :session_activation, user: }
it 'calls create! and purge_old' do
allow(described_class).to receive(:create!).with(**options)
allow(described_class).to receive(:purge_old)
described_class.activate(**options)
expect(described_class).to have_received(:create!).with(**options)
expect(described_class).to have_received(:purge_old)
around do |example|
original = Rails.configuration.x.max_session_activations
Rails.configuration.x.max_session_activations = 1
example.run
Rails.configuration.x.max_session_activations = original
end
it 'returns an instance of SessionActivation' do
expect(described_class.activate(**options)).to be_a described_class
it 'creates a new activation and purges older ones' do
result = described_class.activate(user: user, session_id: '123')
expect(result)
.to be_a(described_class)
.and have_attributes(session_id: '123', user:)
expect { session_activation.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe CreateCollectionService do
subject { described_class.new }
let(:author) { Fabricate.create(:account) }
describe '#call' do
let(:base_params) do
{
name: 'People to follow',
description: 'All my favourites',
sensitive: false,
discoverable: true,
}
end
context 'when given valid parameters' do
it 'creates a new local collection' do
collection = nil
expect do
collection = subject.call(base_params, author)
end.to change(Collection, :count).by(1)
expect(collection).to be_a(Collection)
expect(collection).to be_local
end
context 'when given account ids' do
let(:account_ids) do
Fabricate.times(2, :account).map { |a| a.id.to_s }
end
let(:params) do
base_params.merge(account_ids:)
end
it 'also creates collection items' do
expect do
subject.call(params, author)
end.to change(CollectionItem, :count).by(2)
end
end
context 'when given a tag' do
let(:params) { base_params.merge(tag: '#people') }
context 'when the tag exists' do
let!(:tag) { Fabricate.create(:tag, name: 'people') }
it 'correctly assigns the existing tag' do
collection = subject.call(params, author)
expect(collection.tag).to eq tag
end
end
context 'when the tag does not exist' do
it 'creates a new tag' do
collection = nil
expect do
collection = subject.call(params, author)
end.to change(Tag, :count).by(1)
expect(collection.tag.name).to eq 'people'
end
end
end
end
context 'when given invalid parameters' do
it 'raises an exception' do
expect do
subject.call({}, author)
end.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
end

View File

@ -1,7 +1,7 @@
{
"name": "@mastodon/streaming",
"license": "AGPL-3.0-or-later",
"packageManager": "yarn@4.10.3",
"packageManager": "yarn@4.12.0",
"engines": {
"node": ">=20"
},

351
yarn.lock
View File

@ -334,7 +334,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/parser@npm:7.28.5"
dependencies:
@ -1209,7 +1209,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
@ -1226,32 +1226,25 @@ __metadata:
languageName: node
linkType: hard
"@cacheable/memoize@npm:^2.0.1":
version: 2.0.1
resolution: "@cacheable/memoize@npm:2.0.1"
"@cacheable/memory@npm:^2.0.5":
version: 2.0.5
resolution: "@cacheable/memory@npm:2.0.5"
dependencies:
"@cacheable/utils": "npm:^2.0.1"
checksum: 10c0/40ab683429132654b95edc1229c175c45045c239d647bffa75165ae572cd8da0ee9852dfb7c6baca4e8ecc0e3005c2555222ae02add8206d4a227106b1c5fc8d
"@cacheable/utils": "npm:^2.3.0"
"@keyv/bigmap": "npm:^1.1.0"
hookified: "npm:^1.12.2"
keyv: "npm:^5.5.4"
checksum: 10c0/bef5b26de58c4ca20d7cce457d053766b5fb13de48bf444e0ecf56481a16e6556a194dafc28f41906ae4e6cd053ef3d57686c770b8e7a2d381648505bd673839
languageName: node
linkType: hard
"@cacheable/memory@npm:^2.0.1":
version: 2.0.1
resolution: "@cacheable/memory@npm:2.0.1"
"@cacheable/utils@npm:^2.3.0":
version: 2.3.1
resolution: "@cacheable/utils@npm:2.3.1"
dependencies:
"@cacheable/memoize": "npm:^2.0.1"
"@cacheable/utils": "npm:^2.0.1"
"@keyv/bigmap": "npm:^1.0.0"
hookified: "npm:^1.12.0"
keyv: "npm:^5.5.1"
checksum: 10c0/dde1caf7fe66febe8dc9d32ac4eaee48b6b1e690881adfaf3c41188a00892a20514cd7847e33a64bad09c4c5d6e1377eb8e373b02b4b9fb5fa27e6a67297c625
languageName: node
linkType: hard
"@cacheable/utils@npm:^2.0.1":
version: 2.0.1
resolution: "@cacheable/utils@npm:2.0.1"
checksum: 10c0/63806cca7f60add1f7bd4acee279c723760322316661a9fc6deab9ba7c03569e6cbe744d88159d4e72c1f6d296bba53b53c396d01d52294d45c9fed04c416ca4
hashery: "npm:^1.2.0"
keyv: "npm:^5.5.4"
checksum: 10c0/04802bc11293ff569204e5f143cd11314856e3453de3e5757068cfd9df5c974a80aa9974c8400d88b22de3af70a7d8511d2d7fe927356365f41b765693a4c4bb
languageName: node
linkType: hard
@ -2682,12 +2675,15 @@ __metadata:
languageName: node
linkType: hard
"@keyv/bigmap@npm:^1.0.0":
version: 1.0.2
resolution: "@keyv/bigmap@npm:1.0.2"
"@keyv/bigmap@npm:^1.1.0":
version: 1.3.0
resolution: "@keyv/bigmap@npm:1.3.0"
dependencies:
hookified: "npm:^1.12.1"
checksum: 10c0/1fe415265241b015c19891dc6c1909b41a5a033e57339b40f85af27355d2f52b52df01795a3f7ba37d3ec2b67e147c05914965775254ff8dbd1701adab45208a
hashery: "npm:^1.2.0"
hookified: "npm:^1.13.0"
peerDependencies:
keyv: ^5.5.4
checksum: 10c0/68fe63451097067d8359dc25b7e5b832fe9d99493ca32602686026c8d14c9ca7ecaf19312df9420c7f54df8de1b26e7305f9189fa364a82f39730710eb895f9e
languageName: node
linkType: hard
@ -4831,62 +4827,62 @@ __metadata:
linkType: hard
"@vitest/browser-playwright@npm:^4.0.5":
version: 4.0.7
resolution: "@vitest/browser-playwright@npm:4.0.7"
version: 4.0.13
resolution: "@vitest/browser-playwright@npm:4.0.13"
dependencies:
"@vitest/browser": "npm:4.0.7"
"@vitest/mocker": "npm:4.0.7"
"@vitest/browser": "npm:4.0.13"
"@vitest/mocker": "npm:4.0.13"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
playwright: "*"
vitest: 4.0.7
vitest: 4.0.13
peerDependenciesMeta:
playwright:
optional: false
checksum: 10c0/85969557a441c141b14bc13e3b2f1b70f7e7aa3eb8a63489b4b3af253b3fcd8019293cb56e1dea50f338eca9bc845e21ec85f1c88ab6bc3ee494fbdacac153a3
checksum: 10c0/5a387eb02534736a25cfff442e66e8c41ef97f0db744ffe8360e484af61d66db793cb44ba8681471b8c21ba509db1775f1ba688bc7f50325eee76918773848cb
languageName: node
linkType: hard
"@vitest/browser@npm:4.0.7, @vitest/browser@npm:^4.0.5":
version: 4.0.7
resolution: "@vitest/browser@npm:4.0.7"
"@vitest/browser@npm:4.0.13, @vitest/browser@npm:^4.0.5":
version: 4.0.13
resolution: "@vitest/browser@npm:4.0.13"
dependencies:
"@vitest/mocker": "npm:4.0.7"
"@vitest/utils": "npm:4.0.7"
magic-string: "npm:^0.30.19"
"@vitest/mocker": "npm:4.0.13"
"@vitest/utils": "npm:4.0.13"
magic-string: "npm:^0.30.21"
pixelmatch: "npm:7.1.0"
pngjs: "npm:^7.0.0"
sirv: "npm:^3.0.2"
tinyrainbow: "npm:^3.0.3"
ws: "npm:^8.18.3"
peerDependencies:
vitest: 4.0.7
checksum: 10c0/22b6d666f7ae6220dea8d55afd787b435f4dc19656d22c7892f7caafd3c26b62fc87a091384c6044aa49b84c187581f6cad44fd8a65f1583abef5197a162b5c6
vitest: 4.0.13
checksum: 10c0/22c9297888a7288717cad706ca08159b3af05337a2f9b8da98fe74e683d534c8d816e40fece96f218d223a54c06762c5aa2a5db23ce8565c174ab9a70aade7f0
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:^4.0.5":
version: 4.0.7
resolution: "@vitest/coverage-v8@npm:4.0.7"
version: 4.0.13
resolution: "@vitest/coverage-v8@npm:4.0.13"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.7"
ast-v8-to-istanbul: "npm:^0.3.5"
"@vitest/utils": "npm:4.0.13"
ast-v8-to-istanbul: "npm:^0.3.8"
debug: "npm:^4.4.3"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^5.0.6"
istanbul-reports: "npm:^3.2.0"
magicast: "npm:^0.3.5"
std-env: "npm:^3.9.0"
magicast: "npm:^0.5.1"
std-env: "npm:^3.10.0"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
"@vitest/browser": 4.0.7
vitest: 4.0.7
"@vitest/browser": 4.0.13
vitest: 4.0.13
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10c0/4a34c6de4d1e8173856af078c053e5a6d4a3ad0085fd613fddbe5067b7083d6d11858788a994a3bd427630ddd56cc5eb948b59b425b2c7ba7dd73094d2f1844f
checksum: 10c0/dd462b13605fca62d20cb5a4f9d7cfda2bfa5e77aedc16ad5a633d8dabb24f68e96382ac4d16c2fdcddb45e7c4717e558f5ac51a70c64857f5e89d12d8700823
languageName: node
linkType: hard
@ -4903,17 +4899,17 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/expect@npm:4.0.7"
"@vitest/expect@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/expect@npm:4.0.13"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.7"
"@vitest/utils": "npm:4.0.7"
chai: "npm:^6.0.1"
"@vitest/spy": "npm:4.0.13"
"@vitest/utils": "npm:4.0.13"
chai: "npm:^6.2.1"
tinyrainbow: "npm:^3.0.3"
checksum: 10c0/366d7be563149b6143a6f275ae37b77ac7b13d96cd5b0992bfece5c801c1ed1cb7f4a1a1921e9fa5c47f8ad4e73bdfb9f3f362acc42be34cedcb907020c313a2
checksum: 10c0/1cd7dc02cb650d024826f2e20260d23c2b9ab6733341045ffb59be7af73402eecd2422198d7e4eac609710730b6d11f0faf22af0c074d65445ab88d9da7f6556
languageName: node
linkType: hard
@ -4936,13 +4932,13 @@ __metadata:
languageName: node
linkType: hard
"@vitest/mocker@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/mocker@npm:4.0.7"
"@vitest/mocker@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/mocker@npm:4.0.13"
dependencies:
"@vitest/spy": "npm:4.0.7"
"@vitest/spy": "npm:4.0.13"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.19"
magic-string: "npm:^0.30.21"
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
@ -4951,7 +4947,7 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10c0/a500d2eca0e8b43f63358bd102e1203f3e478c0896ebe41dcdac0ab048e991736dc053bd4129dcf62ba94d4d3d2e43793175cd7deb6552cf54a2a9c8a5bab77b
checksum: 10c0/667ec4fbb77a28ede1b055b9d962beed92c2dd2d83b7bab1ed22239578a7b399180a978e26ef136301c0bc7c57c75ca178cda55ec94081856437e3b4be4a3e19
languageName: node
linkType: hard
@ -4964,33 +4960,33 @@ __metadata:
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/pretty-format@npm:4.0.7"
"@vitest/pretty-format@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/pretty-format@npm:4.0.13"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10c0/4084355dbc7b1b9ee4b777adbbc44833dfd4c4a4bb2de8cb5ef28f490bf1c699eb31820157692dc87504b26e6b7bd931d0f316472895bfbc608327a671646032
checksum: 10c0/c32ebd3457fd4b92fa89800b0ddaa2ca7de84df75be3c64f87ace006f3a3ec546a6ffd4c06f88e3161e80f9e10c83dfee61150e682eaa5a1871240d98c7ef0eb
languageName: node
linkType: hard
"@vitest/runner@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/runner@npm:4.0.7"
"@vitest/runner@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/runner@npm:4.0.13"
dependencies:
"@vitest/utils": "npm:4.0.7"
"@vitest/utils": "npm:4.0.13"
pathe: "npm:^2.0.3"
checksum: 10c0/717d7ce765eba1493051b309f82755a4b1d8594de6cd9d036864c9464dadc604c703388d96e070fe843b0216d2a7b66e59d18ad3db8055990114b3506bc172bf
checksum: 10c0/e9f95b8a413f875123e5c32322dd92bd523d6e3ba25b054f0e865f42e01f82666b847535fe5ea2ff3238faa2df16cefc7e5845d3d5ccfecb3a96ab924d31e760
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/snapshot@npm:4.0.7"
"@vitest/snapshot@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/snapshot@npm:4.0.13"
dependencies:
"@vitest/pretty-format": "npm:4.0.7"
magic-string: "npm:^0.30.19"
"@vitest/pretty-format": "npm:4.0.13"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10c0/0382303038ebc58d419047fd329f848336e06b292839ea6ec05063092ce32464eb6a21c00e6487541e42b8155fa0c10b8cc2f6445ff85256b7d73feeb4a8afef
checksum: 10c0/ad3fbe9ff30bc294811556f958e0014cb03888ea06ac7c05ab41e20c582fe8e27d8f4176aaf8a8e230fc6377124af30f5622173fb459b70a30ff9dd622664be2
languageName: node
linkType: hard
@ -5003,18 +4999,18 @@ __metadata:
languageName: node
linkType: hard
"@vitest/spy@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/spy@npm:4.0.7"
checksum: 10c0/88c8fdffa54cdfb9f4157316d1fb3308ad1630791881866878c7cbf837ad8e6e6aa79041c82635b2598ed551cf93409d34a434c87b779a30a66f55221de636fa
"@vitest/spy@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/spy@npm:4.0.13"
checksum: 10c0/64dc4c496eb9aacd3137beedccdb3265c895f8cd2626b3f76d7324ad944be5b1567ede2652eee407991796879270a63abdec4453c73185e637a1d7ff9cd1a009
languageName: node
linkType: hard
"@vitest/ui@npm:^4.0.5":
version: 4.0.7
resolution: "@vitest/ui@npm:4.0.7"
version: 4.0.13
resolution: "@vitest/ui@npm:4.0.13"
dependencies:
"@vitest/utils": "npm:4.0.7"
"@vitest/utils": "npm:4.0.13"
fflate: "npm:^0.8.2"
flatted: "npm:^3.3.3"
pathe: "npm:^2.0.3"
@ -5022,8 +5018,8 @@ __metadata:
tinyglobby: "npm:^0.2.15"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
vitest: 4.0.7
checksum: 10c0/e7173da36e9549a8b04ee4015a64882075aa9f49a59cb135616d22c6d881244c938cf41f6ef149d7e1f42ff414dc72f2bb23f5334b4285801adf83280b23a279
vitest: 4.0.13
checksum: 10c0/7656762bc6a9c99850639d0809ada53ad4b842e4d9a8c7b82987b60bcf1675c98c077516a3777fce9580255538d0d050c92cb1e6f6296af6365f2387d7a972b9
languageName: node
linkType: hard
@ -5038,13 +5034,13 @@ __metadata:
languageName: node
linkType: hard
"@vitest/utils@npm:4.0.7":
version: 4.0.7
resolution: "@vitest/utils@npm:4.0.7"
"@vitest/utils@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/utils@npm:4.0.13"
dependencies:
"@vitest/pretty-format": "npm:4.0.7"
"@vitest/pretty-format": "npm:4.0.13"
tinyrainbow: "npm:^3.0.3"
checksum: 10c0/a2305c5117a30f1685f362767a0e0cc47265f3602469641f6eb01b4b708e2b1c35c33ccf480314348b21978d70a78311cce3f5bdd09de6456d528c5469093217
checksum: 10c0/1b64872e82a652f11bfd813c0140eaae9b6e4ece39fc0e460ab2b3111b925892f1128f3b27f3a280471cfc404bb9c9289c59f8ca5387950ab35d024d154e9ec1
languageName: node
linkType: hard
@ -5343,7 +5339,7 @@ __metadata:
languageName: node
linkType: hard
"ast-v8-to-istanbul@npm:^0.3.5":
"ast-v8-to-istanbul@npm:^0.3.8":
version: 0.3.8
resolution: "ast-v8-to-istanbul@npm:0.3.8"
dependencies:
@ -5700,16 +5696,16 @@ __metadata:
languageName: node
linkType: hard
"cacheable@npm:^2.0.1":
version: 2.0.1
resolution: "cacheable@npm:2.0.1"
"cacheable@npm:^2.2.0":
version: 2.2.0
resolution: "cacheable@npm:2.2.0"
dependencies:
"@cacheable/memoize": "npm:^2.0.1"
"@cacheable/memory": "npm:^2.0.1"
"@cacheable/utils": "npm:^2.0.1"
hookified: "npm:^1.12.0"
keyv: "npm:^5.5.1"
checksum: 10c0/c4c16af5997850531a02b0efa150d3a06fff9560eca15a16e4042038a63487e967f00f5f1b63310523877b2ac4038e732faf0e707a5ff5d10cb861c01ed67ca8
"@cacheable/memory": "npm:^2.0.5"
"@cacheable/utils": "npm:^2.3.0"
hookified: "npm:^1.13.0"
keyv: "npm:^5.5.4"
qified: "npm:^0.5.2"
checksum: 10c0/39b09e68b0a3da6c53dba1ed10dd13b63bf03f1fcb93ee941dd324d758e2d84466f1acb0c760fa78b23377be15ee58dc9acfb1fde85a89d2483d195b2f75e249
languageName: node
linkType: hard
@ -5779,10 +5775,10 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^6.0.1":
version: 6.2.0
resolution: "chai@npm:6.2.0"
checksum: 10c0/a4b7d7f5907187e09f1847afa838d6d1608adc7d822031b7900813c4ed5d9702911ac2468bf290676f22fddb3d727b1be90b57c1d0a69b902534ee29cdc6ff8a
"chai@npm:^6.2.1":
version: 6.2.1
resolution: "chai@npm:6.2.1"
checksum: 10c0/0c2d84392d7c6d44ca5d14d94204f1760e22af68b83d1f4278b5c4d301dabfc0242da70954dd86b1eda01e438f42950de6cf9d569df2103678538e4014abe50b
languageName: node
linkType: hard
@ -5959,7 +5955,7 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^14.0.1":
"commander@npm:^14.0.2":
version: 14.0.2
resolution: "commander@npm:14.0.2"
checksum: 10c0/245abd1349dbad5414cb6517b7b5c584895c02c4f7836ff5395f301192b8566f9796c82d7bd6c92d07eba8775fe4df86602fca5d86d8d10bcc2aded1e21c2aeb
@ -7410,9 +7406,9 @@ __metadata:
linkType: hard
"fake-indexeddb@npm:^6.0.1":
version: 6.2.4
resolution: "fake-indexeddb@npm:6.2.4"
checksum: 10c0/53b7e9e8f7e413c1a45a4f80f6deda00ef02676cc3a4457e73abff06720ec6b4f4f63fd65c1ef6b03298a1e7e4684d4bf65137a8b9ac166dcfade4b2acf229b4
version: 6.2.5
resolution: "fake-indexeddb@npm:6.2.5"
checksum: 10c0/6c5e2fe84a61daa06d7ad63699d1041fe61847f15f92db12415634b3db94f363a64be9e08a3c3c4434af9c3c0132086b85c4d5dc5e8e06edae1e7daf70ce1f3c
languageName: node
linkType: hard
@ -7513,12 +7509,12 @@ __metadata:
languageName: node
linkType: hard
"file-entry-cache@npm:^10.1.4":
version: 10.1.4
resolution: "file-entry-cache@npm:10.1.4"
"file-entry-cache@npm:^11.1.0":
version: 11.1.1
resolution: "file-entry-cache@npm:11.1.1"
dependencies:
flat-cache: "npm:^6.1.13"
checksum: 10c0/78a7d6b257c620374a8fc5280f14acffc7bd5cb5d39a5bd3509c640f17209f5194eff6e3b476d19db7cfbe9f97abe85ec8d33260f7ed94225efb2a95a68841a6
flat-cache: "npm:^6.1.19"
checksum: 10c0/aa639f5dd578f63984a941f34b112180a4bd9d091d03970752437958158932957fc576861b9fbcf4d6eceaeb0779ad5359befdc321cc1bac59aa6f56f6b1d205
languageName: node
linkType: hard
@ -7590,14 +7586,14 @@ __metadata:
languageName: node
linkType: hard
"flat-cache@npm:^6.1.13":
version: 6.1.14
resolution: "flat-cache@npm:6.1.14"
"flat-cache@npm:^6.1.19":
version: 6.1.19
resolution: "flat-cache@npm:6.1.19"
dependencies:
cacheable: "npm:^2.0.1"
cacheable: "npm:^2.2.0"
flatted: "npm:^3.3.3"
hookified: "npm:^1.12.0"
checksum: 10c0/e17eda47414b4742bc557650788f18255068621afb66b23dfc6a47b3ef3e6c366ede7329e71406bf331ef6f4a3b243040803eb175560b4ceb204779066ba6e92
hookified: "npm:^1.13.0"
checksum: 10c0/80c2d3c6ff2b7327920dacf2ab57e70ba4e865120209e41295689f029fcec17a6b49892ded7ce758d968d2b097fa2f9ab4e52923d971f3cdc90af9faba4680fd
languageName: node
linkType: hard
@ -8044,6 +8040,15 @@ __metadata:
languageName: node
linkType: hard
"hashery@npm:^1.2.0":
version: 1.2.0
resolution: "hashery@npm:1.2.0"
dependencies:
hookified: "npm:^1.13.0"
checksum: 10c0/57905ae4bcb12faedf222b1f39cc05424ae2a2bba1f613b9c582a4e5012b8361c14a25a5a0c16da7eca70ee8338ad2924d6b9566667014c927a36d4b90ad5b72
languageName: node
linkType: hard
"hasown@npm:^2.0.2":
version: 2.0.2
resolution: "hasown@npm:2.0.2"
@ -8106,10 +8111,10 @@ __metadata:
languageName: node
linkType: hard
"hookified@npm:^1.12.0, hookified@npm:^1.12.1":
version: 1.12.1
resolution: "hookified@npm:1.12.1"
checksum: 10c0/fe8d74ee49d1f79677dcdff7606eeb731f7a7dc59f61ec2141a11e3bb94ff6532f870649b900fa9f68568f410c504a338d8732e4d1abe61b426e645c37862e50
"hookified@npm:^1.12.2, hookified@npm:^1.13.0":
version: 1.13.0
resolution: "hookified@npm:1.13.0"
checksum: 10c0/26718a60385ab95f20173323c175a23b06efcc1fac613c51714c9c38038c7395ed52d3bea660840c8362d92dc38022ae4469c2a531728f6116f4df53f70505e7
languageName: node
linkType: hard
@ -9073,12 +9078,12 @@ __metadata:
languageName: node
linkType: hard
"keyv@npm:^5.5.1":
version: 5.5.2
resolution: "keyv@npm:5.5.2"
"keyv@npm:^5.5.4":
version: 5.5.4
resolution: "keyv@npm:5.5.4"
dependencies:
"@keyv/serialize": "npm:^1.1.1"
checksum: 10c0/b0a224210e8bbc4a5913535aa7cc8552809dc81ad67311cc78a2ccfe5485b82b23b8e28ea3a1202e8352c18dd2a0b12b49cda5bb933958d2dcf88042db54c9d0
checksum: 10c0/8dad7f61022c6348c4c691a19468b7c238198252edbd3cc08277d95253c137af7ce5ffd763b6ffded4a75cbe03dc3134f1adcd3dd26c5767c2c9c254e3b39001
languageName: node
linkType: hard
@ -9202,10 +9207,10 @@ __metadata:
linkType: hard
"lint-staged@npm:^16.2.6":
version: 16.2.6
resolution: "lint-staged@npm:16.2.6"
version: 16.2.7
resolution: "lint-staged@npm:16.2.7"
dependencies:
commander: "npm:^14.0.1"
commander: "npm:^14.0.2"
listr2: "npm:^9.0.5"
micromatch: "npm:^4.0.8"
nano-spawn: "npm:^2.0.0"
@ -9214,7 +9219,7 @@ __metadata:
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10c0/6bae38082a0fcb3f699b144d1a4b85394f259f17a1f8a58b22122b9f1c6bb5e8340d6ee4bff12e52dbc4267377d6dde9e5c206157f381f1924a2640717f769c1
checksum: 10c0/9a677c21a8112d823ae5bc565ba2c9e7b803786f2a021c46827a55fe44ed59def96edb24fc99c06a2545cdbbf366022ad82addcb3bf60c712f3b98ef92069717
languageName: node
linkType: hard
@ -9378,7 +9383,7 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.0, magic-string@npm:^0.30.17, magic-string@npm:^0.30.19, magic-string@npm:~0.30.11":
"magic-string@npm:^0.30.0, magic-string@npm:^0.30.17, magic-string@npm:^0.30.21, magic-string@npm:~0.30.11":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
dependencies:
@ -9387,14 +9392,14 @@ __metadata:
languageName: node
linkType: hard
"magicast@npm:^0.3.5":
version: 0.3.5
resolution: "magicast@npm:0.3.5"
"magicast@npm:^0.5.1":
version: 0.5.1
resolution: "magicast@npm:0.5.1"
dependencies:
"@babel/parser": "npm:^7.25.4"
"@babel/types": "npm:^7.25.4"
source-map-js: "npm:^1.2.0"
checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
source-map-js: "npm:^1.2.1"
checksum: 10c0/a00bbf3688b9b3e83c10b3bfe3f106cc2ccbf20c4f2dc1c9020a10556dfe0a6a6605a445ee8e86a6e2b484ec519a657b5e405532684f72678c62e4c0d32f962c
languageName: node
linkType: hard
@ -11156,6 +11161,15 @@ __metadata:
languageName: node
linkType: hard
"qified@npm:^0.5.2":
version: 0.5.2
resolution: "qified@npm:0.5.2"
dependencies:
hookified: "npm:^1.13.0"
checksum: 10c0/4234ba1c0d3b14f31752a39f6788b2490cebad57abd1ce0cee0e04d2fe04a234463785d47559d364affe4d1578aad06fa2fd67b87044688708bf56d4a18ce44a
languageName: node
linkType: hard
"qs@npm:^6.14.0":
version: 6.14.0
resolution: "qs@npm:6.14.0"
@ -12475,7 +12489,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@ -12648,10 +12662,10 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^3.9.0":
version: 3.9.0
resolution: "std-env@npm:3.9.0"
checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50
"std-env@npm:^3.10.0":
version: 3.10.0
resolution: "std-env@npm:3.10.0"
checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f
languageName: node
linkType: hard
@ -12996,8 +13010,8 @@ __metadata:
linkType: hard
"stylelint@npm:^16.19.1":
version: 16.25.0
resolution: "stylelint@npm:16.25.0"
version: 16.26.0
resolution: "stylelint@npm:16.26.0"
dependencies:
"@csstools/css-parser-algorithms": "npm:^3.0.5"
"@csstools/css-tokenizer": "npm:^3.0.4"
@ -13012,7 +13026,7 @@ __metadata:
debug: "npm:^4.4.3"
fast-glob: "npm:^3.3.3"
fastest-levenshtein: "npm:^1.0.16"
file-entry-cache: "npm:^10.1.4"
file-entry-cache: "npm:^11.1.0"
global-modules: "npm:^2.0.0"
globby: "npm:^11.1.0"
globjoin: "npm:^0.1.4"
@ -13039,7 +13053,7 @@ __metadata:
write-file-atomic: "npm:^5.0.1"
bin:
stylelint: bin/stylelint.mjs
checksum: 10c0/80fa44ff5197419647306d9b2cf3804c10255ac30a96bb3390f2a3f1debee80e2e1cde81bb4e87d9179ababc9cc0ad6c362661835ee1d32a509b7fe44a8d3dd6
checksum: 10c0/6f501ff051aee4fc7713635c98bf6837f889b22fe86152cfed20365ffeee0acf9d751f94ff265433b532b2a1ab7a228fc1fda3f507859acb57a689268939553d
languageName: node
linkType: hard
@ -14059,23 +14073,23 @@ __metadata:
linkType: hard
"vitest@npm:^4.0.5":
version: 4.0.7
resolution: "vitest@npm:4.0.7"
version: 4.0.13
resolution: "vitest@npm:4.0.13"
dependencies:
"@vitest/expect": "npm:4.0.7"
"@vitest/mocker": "npm:4.0.7"
"@vitest/pretty-format": "npm:4.0.7"
"@vitest/runner": "npm:4.0.7"
"@vitest/snapshot": "npm:4.0.7"
"@vitest/spy": "npm:4.0.7"
"@vitest/utils": "npm:4.0.7"
"@vitest/expect": "npm:4.0.13"
"@vitest/mocker": "npm:4.0.13"
"@vitest/pretty-format": "npm:4.0.13"
"@vitest/runner": "npm:4.0.13"
"@vitest/snapshot": "npm:4.0.13"
"@vitest/spy": "npm:4.0.13"
"@vitest/utils": "npm:4.0.13"
debug: "npm:^4.4.3"
es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2"
magic-string: "npm:^0.30.19"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
picomatch: "npm:^4.0.3"
std-env: "npm:^3.9.0"
std-env: "npm:^3.10.0"
tinybench: "npm:^2.9.0"
tinyexec: "npm:^0.3.2"
tinyglobby: "npm:^0.2.15"
@ -14084,17 +14098,20 @@ __metadata:
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/debug": ^4.1.12
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.7
"@vitest/browser-preview": 4.0.7
"@vitest/browser-webdriverio": 4.0.7
"@vitest/ui": 4.0.7
"@vitest/browser-playwright": 4.0.13
"@vitest/browser-preview": 4.0.13
"@vitest/browser-webdriverio": 4.0.13
"@vitest/ui": 4.0.13
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
"@opentelemetry/api":
optional: true
"@types/debug":
optional: true
"@types/node":
@ -14113,7 +14130,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10c0/d5312e11e9ffbaf239fa5cb5d43e81a43adbdb085bf113c237eff3531805a347e6587ad1dd368a5e9639ff23f8f48650e689fb49c3cf9375d922ef2767a6416a
checksum: 10c0/8582ab1848d5d7dbbac0b3a5eae2625f44d0db887f73da2ee8f588fb13c66fe8ea26dac05c26ebb43673b735bc246764f52969f7c7e25455dfb7c6274659ae2c
languageName: node
linkType: hard