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

This commit is contained in:
Claire
2025-11-30 16:40:18 +01:00
52 changed files with 806 additions and 623 deletions
@@ -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,
};
}
@@ -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;
@@ -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 });
}
}
@@ -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 });
}
}
@@ -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();
}
@@ -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')}
/>
);
@@ -38,7 +38,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchBlocks());
}
@@ -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();
@@ -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));
}
@@ -45,7 +45,7 @@ class FollowRequests extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchFollowRequests());
}
@@ -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;
@@ -40,7 +40,7 @@ class Mutes extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchMutes());
}
@@ -34,7 +34,7 @@ class PinnedStatuses extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
componentDidMount () {
this.props.dispatch(fetchPinnedStatuses());
}
@@ -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));
}
@@ -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'])} />}
</>
);
}
}
@@ -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;
@@ -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')}
/>
);
@@ -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);
});
};
@@ -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>
);
})}
@@ -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' />
@@ -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);
@@ -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);
}
@@ -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>
);
};
+6
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",
+1
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",
+22 -22
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.",
+30 -30
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} 已被移動。",
@@ -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;
@@ -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;
@@ -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;
@@ -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