mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 10:00:50 +00:00
Compare commits
19 Commits
085b4814f9
...
0e27418173
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e27418173 | ||
|
|
002632c3bb | ||
|
|
81510455d1 | ||
|
|
ee7e756e89 | ||
|
|
f87f30c1ac | ||
|
|
1757a0f0f3 | ||
|
|
cb4f1cc89c | ||
|
|
00163e89bf | ||
|
|
59e48657cf | ||
|
|
384594f462 | ||
|
|
cd9d166312 | ||
|
|
6f4f9942b9 | ||
|
|
f2a2d73e46 | ||
|
|
83b68ebad1 | ||
|
|
8902ba1fd5 | ||
|
|
34dbba26cf | ||
|
|
d6151cfd56 | ||
|
|
922701e90f | ||
|
|
c5a075b6c2 |
2
Gemfile
2
Gemfile
|
|
@ -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'
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ module Settings
|
|||
redirect_to new_settings_two_factor_authentication_confirmation_path
|
||||
end
|
||||
|
||||
def destroy
|
||||
current_user.disable_otp_login!
|
||||
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_otp_not_enabled
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ module Settings
|
|||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? }
|
||||
before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? }
|
||||
|
||||
def index; end
|
||||
|
|
@ -85,10 +84,6 @@ module Settings
|
|||
|
||||
private
|
||||
|
||||
def redirect_invalid_otp
|
||||
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') }
|
||||
end
|
||||
|
||||
def redirect_invalid_webauthn
|
||||
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ module Settings
|
|||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_challenge!, only: :disable
|
||||
before_action :require_otp_enabled
|
||||
before_action :require_two_factor_enabled, only: :disable
|
||||
|
||||
def index; end
|
||||
|
||||
|
|
@ -16,13 +16,13 @@ module Settings
|
|||
current_user.disable_two_factor!
|
||||
UserMailer.two_factor_disabled(current_user).deliver_later!
|
||||
|
||||
redirect_to settings_otp_authentication_path, flash: { notice: I18n.t('two_factor_authentication.disabled_success') }
|
||||
redirect_to settings_two_factor_authentication_methods_path, flash: { notice: I18n.t('two_factor_authentication.disabled_success') }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_otp_enabled
|
||||
redirect_to settings_otp_authentication_path unless current_user.otp_enabled?
|
||||
def require_two_factor_enabled
|
||||
redirect_to settings_otp_authentication_path unless current_user.two_factor_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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'])} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
316
app/javascript/mastodon/features/status/components/card.tsx
Normal file
316
app/javascript/mastodon/features/status/components/card.tsx
Normal 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;
|
||||
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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} 已被移動。",
|
||||
|
|
|
|||
|
|
@ -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%
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ class SessionActivation < ApplicationRecord
|
|||
end
|
||||
|
||||
def activate(**)
|
||||
activation = create!(**)
|
||||
purge_old
|
||||
activation
|
||||
create!(**).tap { purge_old }
|
||||
end
|
||||
|
||||
def deactivate(id)
|
||||
|
|
|
|||
|
|
@ -256,6 +256,15 @@ class User < ApplicationRecord
|
|||
otp_required_for_login? || webauthn_credentials.any?
|
||||
end
|
||||
|
||||
def disable_otp_login!
|
||||
return unless otp_required_for_login?
|
||||
|
||||
self.otp_required_for_login = false
|
||||
self.otp_secret = nil
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
def disable_two_factor!
|
||||
self.otp_required_for_login = false
|
||||
self.otp_secret = nil
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.two_factor_authentication')
|
||||
|
||||
- content_for :heading_actions do
|
||||
= link_to t('two_factor_authentication.disable'), disable_settings_two_factor_authentication_methods_path, class: 'button button--destructive', method: :post
|
||||
- if current_user.two_factor_enabled?
|
||||
- content_for :heading_actions do
|
||||
= link_to t('two_factor_authentication.disable'), disable_settings_two_factor_authentication_methods_path, class: 'button button--destructive', method: :post
|
||||
|
||||
%p.hint
|
||||
%span.positive-hint
|
||||
= material_symbol 'check'
|
||||
|
||||
= t 'two_factor_authentication.enabled'
|
||||
%p.hint
|
||||
%span.positive-hint
|
||||
= material_symbol 'check'
|
||||
|
||||
= t 'two_factor_authentication.enabled'
|
||||
|
||||
.table-wrapper
|
||||
%table.table
|
||||
|
|
@ -19,8 +20,13 @@
|
|||
%tbody
|
||||
%tr
|
||||
%td= t('two_factor_authentication.otp')
|
||||
%td
|
||||
= table_link_to 'edit', t('two_factor_authentication.edit'), settings_otp_authentication_path, method: :post
|
||||
- if current_user.otp_enabled?
|
||||
%td
|
||||
= table_link_to 'edit', t('two_factor_authentication.edit'), settings_otp_authentication_path, method: :get
|
||||
= table_link_to 'delete', t('otp_authentication.delete'), settings_otp_authentication_path, method: :delete, data: { confirm: t('otp_authentication.delete_confirmation') }
|
||||
- else
|
||||
%td
|
||||
= table_link_to 'add', t('two_factor_authentication.add'), settings_otp_authentication_path, method: :get
|
||||
%tr
|
||||
%td= t('two_factor_authentication.webauthn')
|
||||
- if current_user.webauthn_enabled?
|
||||
|
|
@ -30,12 +36,13 @@
|
|||
%td
|
||||
= table_link_to 'key', t('two_factor_authentication.add'), new_settings_webauthn_credential_path, method: :get
|
||||
|
||||
%hr.spacer/
|
||||
- if current_user.otp_enabled?
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t('two_factor_authentication.recovery_codes')
|
||||
%p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
|
||||
%h3= t('two_factor_authentication.recovery_codes')
|
||||
%p.muted-hint= t('two_factor_authentication.lost_recovery_codes')
|
||||
|
||||
%hr.spacer/
|
||||
%hr.spacer/
|
||||
|
||||
.simple_form
|
||||
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'button button--block'
|
||||
.simple_form
|
||||
= link_to t('two_factor_authentication.generate_recovery_codes'), settings_two_factor_authentication_recovery_codes_path, data: { method: :post }, class: 'button button--block'
|
||||
|
|
|
|||
|
|
@ -1547,5 +1547,4 @@ an:
|
|||
nickname_hint: Escriba la embotada d'a suya nueva clau de seguranza
|
||||
not_enabled: Encara no has activau WebAuthn
|
||||
not_supported: Este navegador no suporta claus de seguranza
|
||||
otp_required: Pa usar claus de seguranza, per favor habilite primero l'autenticación de dople factor.
|
||||
registered_on: Rechistrau lo %{date}
|
||||
|
|
|
|||
|
|
@ -2310,5 +2310,4 @@ ar:
|
|||
nickname_hint: أدخل اسم مستعار لمفتاح الأمان الجديد الخاص بك
|
||||
not_enabled: لم تقم بتفعيل WebAuthn بعد
|
||||
not_supported: هذا المتصفح لا يدعم مفاتيح الأمان
|
||||
otp_required: لاستخدام مفاتيح الأمان، يرجى تفعيل الاستيثاق بعامِلين أولاً.
|
||||
registered_on: تم التسجيل في %{date}
|
||||
|
|
|
|||
|
|
@ -894,4 +894,3 @@ ast:
|
|||
invalid_credential: La llave de seguranza nun ye válida
|
||||
not_enabled: Nun activesti la función WebAuthn
|
||||
not_supported: Esti restolador nun ye compatible coles llaves de seguranza
|
||||
otp_required: Pa usar les llaves de seguranza, activa primero l'autenticación en dos pasos.
|
||||
|
|
|
|||
|
|
@ -2265,5 +2265,4 @@ be:
|
|||
nickname_hint: Увядзіце псеўданім вашага новага ключа бяспекі
|
||||
not_enabled: Вы яшчэ не ўключылі WebAuthn
|
||||
not_supported: Гэты браўзер не падтрымлівае ключы бяспекі
|
||||
otp_required: Каб выкарыстоўваць ключы бяспекі, спачатку ўключыце двухфактарную аўтэнтыфікацыю.
|
||||
registered_on: Зарэгістраваны %{date}
|
||||
|
|
|
|||
|
|
@ -2151,5 +2151,4 @@ bg:
|
|||
nickname_hint: Въведете прякор на новия си ключ за сигурност
|
||||
not_enabled: Още не сте включили WebAuthn
|
||||
not_supported: Този браузър не поддържа ключове за сигурност
|
||||
otp_required: Първо включете двуфакторното удостоверяване, за да използвате ключовете за сигурност.
|
||||
registered_on: Регистрирано на %{date}
|
||||
|
|
|
|||
|
|
@ -922,5 +922,4 @@ br:
|
|||
nickname_hint: Skrivit lesanv hoc'h alc'hwez surentez nevez
|
||||
not_enabled: WebAuthn n'eo ket aotreet ganeoc'h c'hoazh
|
||||
not_supported: Alc'hwezioù surentez a zo diembreg gant ar merdeer-se
|
||||
otp_required: Evit implijout alc'hwezioù surentez, aotrit dilesadur dre eil-elfenn da gentañ.
|
||||
registered_on: Enrollet d'ar %{date}
|
||||
|
|
|
|||
|
|
@ -2151,5 +2151,4 @@ ca:
|
|||
nickname_hint: Introdueix el sobrenom de la teva clau de seguretat nova
|
||||
not_enabled: Encara no has activat WebAuthn
|
||||
not_supported: Aquest navegador no suporta claus de seguretat
|
||||
otp_required: Per a usar claus de seguretat, activeu primer l'autenticació de dos factors.
|
||||
registered_on: Registrat en %{date}
|
||||
|
|
|
|||
|
|
@ -1001,5 +1001,4 @@ ckb:
|
|||
nickname_hint: نازناوی کلیلی ئاسایشی نوێت تێبنووسە
|
||||
not_enabled: تۆ هێشتا WebAuthnت چالاک نەکردووە
|
||||
not_supported: ئەم وێبگەڕە پشتگیری کلیلەکانی پاراستن ناکات
|
||||
otp_required: بۆ بەکارهێنانی کلیلەکانی پاراستن تکایە سەرەتا سەلماندنی دوو-فاکتەر چالاک بکە.
|
||||
registered_on: تۆمارکراو لە %{date}
|
||||
|
|
|
|||
|
|
@ -1015,5 +1015,4 @@ co:
|
|||
nickname_hint: Entrate u nome di a vostra nova chjave di sicurità
|
||||
not_enabled: Ùn avete micca attivatu WebAuthn
|
||||
not_supported: E chjave di sicurità ùn marchjanu micca cù quessu navigatore
|
||||
otp_required: Per utilizà una chjave di sicurità duvete attivà l'identificazione à dui fattori prima.
|
||||
registered_on: Arregistrata %{date}
|
||||
|
|
|
|||
|
|
@ -2265,5 +2265,4 @@ cs:
|
|||
nickname_hint: Zadejte přezdívku nového bezpečnostního klíče
|
||||
not_enabled: Zatím jste nepovolili WebAuthn
|
||||
not_supported: Tento prohlížeč nepodporuje bezpečnostní klíče
|
||||
otp_required: Pro použití bezpečnostních klíčů prosím nejprve zapněte dvoufázové ověřování.
|
||||
registered_on: Přidán %{date}
|
||||
|
|
|
|||
|
|
@ -2353,5 +2353,4 @@ cy:
|
|||
nickname_hint: Rhowch lysenw eich allwedd ddiogelwch newydd
|
||||
not_enabled: Nid ydych wedi galluogi WebAuthn eto
|
||||
not_supported: Nid yw'r porwr hwn yn cynnal allweddi diogelwch
|
||||
otp_required: I ddefnyddio allweddi diogelwch, galluogwch ddilysu dau ffactor yn gyntaf.
|
||||
registered_on: Cofrestrwyd ar %{date}
|
||||
|
|
|
|||
|
|
@ -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å
|
||||
|
|
@ -2177,5 +2177,4 @@ da:
|
|||
nickname_hint: Angiv kaldenavnet på din nye sikkerhedsnøgle
|
||||
not_enabled: Du har endnu ikke aktiveret WebAuthn
|
||||
not_supported: Denne browser understøtter ikke sikkerhedsnøgler
|
||||
otp_required: For at bruge sikkerhedsnøgler skal tofaktorgodkendelse først aktiveres.
|
||||
registered_on: Registreret d. %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ de:
|
|||
nickname_hint: Gib den Spitznamen deines neuen Sicherheitsschlüssels ein
|
||||
not_enabled: Du hast WebAuthn noch nicht aktiviert
|
||||
not_supported: Dieser Browser unterstützt keine Sicherheitsschlüssel
|
||||
otp_required: Um Sicherheitsschlüssel zu verwenden, aktiviere zunächst die Zwei-Faktor-Authentisierung.
|
||||
registered_on: Registriert am %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ el:
|
|||
nickname_hint: Βάλε το ψευδώνυμο του νέου κλειδιού ασφαλείας σου
|
||||
not_enabled: Δεν έχεις ενεργοποιήσει το WebAuthn ακόμα
|
||||
not_supported: Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει κλειδιά ασφαλείας
|
||||
otp_required: Για να χρησιμοποιήσεις κλειδιά ασφαλείας, ενεργοποίησε πρώτα την ταυτοποίηση δύο παραγόντων.
|
||||
registered_on: Εγγραφή στις %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ en-GB:
|
|||
nickname_hint: Enter the nickname of your new security key
|
||||
not_enabled: You haven't enabled WebAuthn yet
|
||||
not_supported: This browser doesn't support security keys
|
||||
otp_required: To use security keys please enable two-factor authentication first.
|
||||
registered_on: Registered on %{date}
|
||||
|
|
|
|||
|
|
@ -1737,6 +1737,8 @@ en:
|
|||
unit: ''
|
||||
otp_authentication:
|
||||
code_hint: Enter the code generated by your authenticator app to confirm
|
||||
delete: Delete
|
||||
delete_confirmation: Are you sure you want to delete your authenticator app from your two-factor authentication methods?
|
||||
description_html: If you enable <strong>two-factor authentication</strong> using an authenticator app, logging in will require you to be in possession of your phone, which will generate tokens for you to enter.
|
||||
enable: Enable
|
||||
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
||||
|
|
@ -2178,5 +2180,4 @@ en:
|
|||
nickname_hint: Enter the nickname of your new security key
|
||||
not_enabled: You haven't enabled WebAuthn yet
|
||||
not_supported: This browser doesn't support security keys
|
||||
otp_required: To use security keys please enable two-factor authentication first.
|
||||
registered_on: Registered on %{date}
|
||||
|
|
|
|||
|
|
@ -2135,5 +2135,4 @@ eo:
|
|||
nickname_hint: Enigu alinomon de via nova sekurecŝlosilo
|
||||
not_enabled: Vi ankoraŭ ne ŝaltis WebAuth
|
||||
not_supported: Ĉi tiu legilo ne povas uzi sekurecŝlosilojn
|
||||
otp_required: Por uzi sekurecŝlosilojn, ebligu 2-faktoran autentigon unue.
|
||||
registered_on: Registrita je %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ es-AR:
|
|||
nickname_hint: Ingresá el apodo de tu nueva llave de seguridad
|
||||
not_enabled: Todavía no habilitaste WebAuthn
|
||||
not_supported: Este navegador web no soporta llaves de seguridad
|
||||
otp_required: Para usar llaves de seguridad, por favor, primero habilitá la autenticación de dos factores.
|
||||
registered_on: Registrado el %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ es-MX:
|
|||
nickname_hint: Introduzca el apodo de su nueva clave de seguridad
|
||||
not_enabled: Aún no has activado WebAuthn
|
||||
not_supported: Este navegador no soporta claves de seguridad
|
||||
otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor.
|
||||
registered_on: Registrado el %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ es:
|
|||
nickname_hint: Introduzca el apodo de su nueva clave de seguridad
|
||||
not_enabled: Aún no has activado WebAuthn
|
||||
not_supported: Este navegador no soporta claves de seguridad
|
||||
otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor.
|
||||
registered_on: Registrado el %{date}
|
||||
|
|
|
|||
|
|
@ -2179,5 +2179,4 @@ et:
|
|||
nickname_hint: Uue turvavõtme hüüdnimi
|
||||
not_enabled: Veebiautentimine pole sisse lülitatud
|
||||
not_supported: See veebilehitseja ei toeta turvavõtmeid
|
||||
otp_required: Turvavõtmete kasutamiseks tuleb eelnevalt sisse lülitada kaheastmeline autentimine.
|
||||
registered_on: Registreeritud %{date}
|
||||
|
|
|
|||
|
|
@ -1967,5 +1967,4 @@ eu:
|
|||
nickname_hint: Sartu zure segurtasun gako berriaren ezizena
|
||||
not_enabled: Ez duzu WebAuthn gaitu oraindik
|
||||
not_supported: Nabigatzaile honek ez ditu segurtasun gakoak onartzen
|
||||
otp_required: Segurtasun gakoak erabili aurretik bi faktoreko autentifikazioa gaitu behar duzu.
|
||||
registered_on: "%{date}(e)an erregistratua"
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ fa:
|
|||
nickname_hint: نام مستعار کلید امنیتی جدیدتان را وارد کنید
|
||||
not_enabled: شما هنوز WebAuthn را فعال نکردهاید
|
||||
not_supported: این مرورگر از کلیدهای امنیتی پشتیبانی نمیکند
|
||||
otp_required: برای استفاده از کلیدهای امنیتی، لطفاً ابتدا تأیید هویت دو عاملی را به کار بیندازید.
|
||||
registered_on: ثبتشده در %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ fi:
|
|||
nickname_hint: Anna uuden suojausaivaimesi lempinimi
|
||||
not_enabled: Et ole vielä ottanut WebAuthn-ohjelmaa käyttöön
|
||||
not_supported: Tämä selain ei tue suojausavaimia
|
||||
otp_required: Jos haluat käyttää suojausavaimia, ota ensin kaksivaiheinen todennus käyttöön.
|
||||
registered_on: Rekisteröity %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ fo:
|
|||
nickname_hint: Skriva eyknevni á tínum nýggja trygdarlykli
|
||||
not_enabled: Tú hevur ikki gjørt WebAuthn virkið enn
|
||||
not_supported: Hesin kagin stuðlar ikki uppundir trygdarlyklar
|
||||
otp_required: Fyri at brúka trygdarlyklar er neyðugt at gera váttan í tveimum stigum virkna fyrst.
|
||||
registered_on: Skrásett %{date}
|
||||
|
|
|
|||
|
|
@ -2165,5 +2165,4 @@ fr-CA:
|
|||
nickname_hint: Entrez le surnom de votre nouvelle clé de sécurité
|
||||
not_enabled: Vous n'avez pas encore activé WebAuthn
|
||||
not_supported: Ce navigateur ne prend pas en charge les clés de sécurité
|
||||
otp_required: Pour utiliser les clés de sécurité, veuillez d'abord activer l'authentification à deux facteurs.
|
||||
registered_on: Inscrit le %{date}
|
||||
|
|
|
|||
|
|
@ -2165,5 +2165,4 @@ fr:
|
|||
nickname_hint: Entrez le surnom de votre nouvelle clé de sécurité
|
||||
not_enabled: Vous n'avez pas encore activé WebAuthn
|
||||
not_supported: Ce navigateur ne prend pas en charge les clés de sécurité
|
||||
otp_required: Pour utiliser les clés de sécurité, veuillez d'abord activer l'authentification à deux facteurs.
|
||||
registered_on: Inscrit le %{date}
|
||||
|
|
|
|||
|
|
@ -2126,5 +2126,4 @@ fy:
|
|||
nickname_hint: Fier de bynamme yn fan jo nije befeiligingskaai
|
||||
not_enabled: Jo hawwe WebAuthn noch net ynskeakele
|
||||
not_supported: Dizze browser stipet gjin befeiligingskaaien
|
||||
otp_required: Om befeiligingskaaien brûke te kinnen, moatte jo earst twa-stapsferifikaasje ynskeakelje.
|
||||
registered_on: Registrearre op %{date}
|
||||
|
|
|
|||
|
|
@ -2311,5 +2311,4 @@ ga:
|
|||
nickname_hint: Cuir isteach leasainm d'eochair shlándála nua
|
||||
not_enabled: Níl WebAuthn cumasaithe agat fós
|
||||
not_supported: Ní thacaíonn an brabhsálaí seo le heochracha slándála
|
||||
otp_required: Chun eochracha slándála a úsáid cumasaigh fíordheimhniú dhá fhachtóir ar dtús.
|
||||
registered_on: Cláraithe ar %{date}
|
||||
|
|
|
|||
|
|
@ -2265,5 +2265,4 @@ gd:
|
|||
nickname_hint: Cuir a-steach far-ainm na h-iuchrach tèarainteachd ùir agad
|
||||
not_enabled: Cha do chuir thu WebAuthn an comas fhathast
|
||||
not_supported: Cha chuir am brabhsair seo taic ri iuchraichean tèarainteachd
|
||||
otp_required: Mus cleachd thu iuchraichean tèarainteachd, feumaidh tu an dearbhadh dà-cheumnach a chur an comas.
|
||||
registered_on: Air a chlàradh %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ gl:
|
|||
nickname_hint: Escribe un alcume para a túa nova chave de seguridade
|
||||
not_enabled: Aínda non tes activado WebAuthn
|
||||
not_supported: Este navegador non ten soporte para chaves de seguridade
|
||||
otp_required: Para usar chaves de seguridade tes que activar primeiro o segundo factor.
|
||||
registered_on: Rexistrado o %{date}
|
||||
|
|
|
|||
|
|
@ -2265,5 +2265,4 @@ he:
|
|||
nickname_hint: הכנס.י כינוי למפתח האבטחה החדש שלך
|
||||
not_enabled: לא אפשרת את WebAuthn עדיין
|
||||
not_supported: דפדפן זה לא תומך במפתחות אבטחה
|
||||
otp_required: על מנת להשתמש במפתחות אבטחה אנא אפשר.י אימות דו-שלבי קודם.
|
||||
registered_on: נרשם ב %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ hu:
|
|||
nickname_hint: Írd be az új biztonsági kulcsod becenevét
|
||||
not_enabled: Még nem engedélyezted a WebAuthn-t
|
||||
not_supported: Ez a böngésző nem támogatja a biztonsági kulcsokat
|
||||
otp_required: A biztonsági kulcsok használatához először engedélyezd a kétlépcsős hitelesítést.
|
||||
registered_on: 'Regisztráció ekkor: %{date}'
|
||||
|
|
|
|||
|
|
@ -2175,5 +2175,4 @@ ia:
|
|||
nickname_hint: Insere le pseudonymo de tu nove clave de securitate
|
||||
not_enabled: Tu ancora non ha activate WebAuthn
|
||||
not_supported: Iste navigator non supporta claves de securitate
|
||||
otp_required: Pro usar le claves de securitate activa prime le authentication de duo factores.
|
||||
registered_on: Inscribite le %{date}
|
||||
|
|
|
|||
|
|
@ -1516,5 +1516,4 @@ id:
|
|||
nickname_hint: Masukkan panggilan kunci keamanan baru Anda
|
||||
not_enabled: Anda belum mengaktifkan WebAuthn
|
||||
not_supported: Peramban ini tidak mendukung kunci keamanan
|
||||
otp_required: Untuk menggunakan kunci keamanan harap aktifkan autentikasi dua-faktor.
|
||||
registered_on: Terdaftar pada %{date}
|
||||
|
|
|
|||
|
|
@ -1820,5 +1820,4 @@ ie:
|
|||
nickname_hint: Scrir li moc-nómine de tui nov clave de securitá
|
||||
not_enabled: Tu ancor ne ha possibilisat WebAuthn
|
||||
not_supported: Ti-ci navigator ne subtene claves de securitá
|
||||
otp_required: Por usar claves de securitá, ples activisar 2-factor autentication.
|
||||
registered_on: Adheret ye %{date}
|
||||
|
|
|
|||
|
|
@ -1892,5 +1892,4 @@ io:
|
|||
nickname_hint: Insertez nometo di vua nova sekuresklefo
|
||||
not_enabled: Vu ne ebligis WebAuthn til nun
|
||||
not_supported: Ca vidilo ne suportas sekuresklefi
|
||||
otp_required: Por uzar sekuresklefi, ebligez dufaktora yurizo unesme.
|
||||
registered_on: Registris ye %{date}
|
||||
|
|
|
|||
|
|
@ -2181,5 +2181,4 @@ is:
|
|||
nickname_hint: Settu inn stuttnefni fyrir nýja öryggislykilinn þinn
|
||||
not_enabled: Þú hefur ennþá ekki virkjað WebAuthn
|
||||
not_supported: Þessi vafri styður ekki öryggislykla
|
||||
otp_required: Til að nota öryggislykla skaltu fyrst virkja tveggja-þátta auðkenningu.
|
||||
registered_on: Skráði sig %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ it:
|
|||
nickname_hint: Inserisci il soprannome della tua nuova chiave di sicurezza
|
||||
not_enabled: Non hai ancora abilitato WebAuthn
|
||||
not_supported: Questo browser non supporta le chiavi di sicurezza
|
||||
otp_required: Per utilizzare le chiavi di sicurezza, prima abilita l'autenticazione a due fattori.
|
||||
registered_on: Registrato il %{date}
|
||||
|
|
|
|||
|
|
@ -2090,5 +2090,4 @@ ja:
|
|||
nickname_hint: セキュリティキーの名前を入力してください
|
||||
not_enabled: まだセキュリティキーを有効にしていません
|
||||
not_supported: このブラウザはセキュリティキーに対応していないようです
|
||||
otp_required: セキュリティキーを使用するには、まず二要素認証を有効にしてください。
|
||||
registered_on: "%{date}に登録"
|
||||
|
|
|
|||
|
|
@ -2134,5 +2134,4 @@ ko:
|
|||
nickname_hint: 새 보안 키의 별명을 입력해 주세요
|
||||
not_enabled: 아직 WebAuthn을 활성화 하지 않았습니다.
|
||||
not_supported: 이 브라우저는 보안 키를 지원하지 않습니다
|
||||
otp_required: 보안 키를 사용하기 위해서는 2단계 인증을 먼저 활성화 해 주세요
|
||||
registered_on: "%{date}에 등록됨"
|
||||
|
|
|
|||
|
|
@ -1542,5 +1542,4 @@ ku:
|
|||
nickname_hint: Bernavka kilîda te ya ewlehiyê a nû têkevê
|
||||
not_enabled: Te hê WebAuthn çalak nekiriye
|
||||
not_supported: Ev gerok piştgiriya kilîtên ewlehiyê nakê
|
||||
otp_required: Ji bo ku tu kilîtên ewlehiyê bikar bînî, ji kerema xwe re pêşî piştrastkirina du-gavî çalak bike.
|
||||
registered_on: Di %{date} dîrokê de tomar bû
|
||||
|
|
|
|||
|
|
@ -2033,5 +2033,4 @@ lad:
|
|||
nickname_hint: Introduska el sovrenombre de tu mueva yave de sigurita
|
||||
not_enabled: Ainda no tienes aktivado WebAuthn
|
||||
not_supported: Este navigador no soporta yaves de sigurita
|
||||
otp_required: Para uzar yaves de sigurita, por favor kapasite primero la autentifikasyon de dos pasos.
|
||||
registered_on: Enrejistrado el %{date}
|
||||
|
|
|
|||
|
|
@ -1354,4 +1354,3 @@ lt:
|
|||
success: Tavo saugumo raktas buvo sėkmingai ištrintas.
|
||||
nickname_hint: Įvesk naujojo saugumo rakto slapyvardį
|
||||
not_enabled: Dar neįjungei WebAuthn
|
||||
otp_required: Norint naudoti saugumo raktus, pirmiausia įjunk dvigubą tapatybės nustatymą.
|
||||
|
|
|
|||
|
|
@ -2133,5 +2133,4 @@ lv:
|
|||
nickname_hint: Ievadi savas jaunās drošības atslēgas segvārdu
|
||||
not_enabled: Tu vel neesi iespējojis WebAuthn
|
||||
not_supported: Šī pārlūkprogramma neatbalsta drošības atslēgas
|
||||
otp_required: Lai izmantotu drošības atslēgas, lūgums vispirms iespējot divpakāpju autentifikāciju.
|
||||
registered_on: Reģistrēts %{date}
|
||||
|
|
|
|||
|
|
@ -1703,5 +1703,4 @@ ms:
|
|||
nickname_hint: Masukkan nama panggilan kunci keselamatan baharu anda
|
||||
not_enabled: Anda belum mendayakan WebAuthn lagi
|
||||
not_supported: Pelayan ini tidak menyokong kunci keselamatan
|
||||
otp_required: Untuk menggunakan kunci keselamatan, sila mengaktifkan pengesahan dua faktor dahulu.
|
||||
registered_on: Didaftar pada %{date}
|
||||
|
|
|
|||
|
|
@ -1694,5 +1694,4 @@ my:
|
|||
nickname_hint: သင့်လုံခြုံရေးကီးအသစ်၏ အမည်ပြောင်ကို ထည့်ပါ။
|
||||
not_enabled: WebAuthn ကို သင် မဖွင့်ရသေးပါ
|
||||
not_supported: ဤဘရောက်ဆာသည် လုံခြုံရေးကီးများကို မပံ့ပိုးပါ
|
||||
otp_required: လုံခြုံရေးကီးများကို အသုံးပြုရန်အတွက် နှစ်ဆင့်ခံလုံခြုံရေးစနစ်စိစစ်ခြင်းကို ဦးစွာဖွင့်ပါ။
|
||||
registered_on: "%{date} တွင် စာရင်းသွင်းထားသည်"
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ nl:
|
|||
nickname_hint: Voer de bijnaam in van jouw nieuwe beveiligingssleutel
|
||||
not_enabled: Je hebt WebAuthn nog niet ingeschakeld
|
||||
not_supported: Deze browser ondersteunt geen beveiligingssleutels
|
||||
otp_required: Om beveiligingssleutels te kunnen gebruiken, moet je eerst tweestapsverificatie inschakelen.
|
||||
registered_on: Geregistreerd op %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ nn:
|
|||
nickname_hint: Skriv inn kallenavnet til din nye sikkerhetsnøkkel
|
||||
not_enabled: Du har ikke aktivert WebAuthn ennå
|
||||
not_supported: Denne nettleseren støtter ikke sikkerhetsnøkler
|
||||
otp_required: For å bruke sikkerhetsnøkler, må du først aktivere to-faktor autentisering.
|
||||
registered_on: Registrert den %{date}
|
||||
|
|
|
|||
|
|
@ -1814,5 +1814,4 @@
|
|||
nickname_hint: Skriv inn kallenavnet til din nye sikkerhetsnøkkel
|
||||
not_enabled: Du har ikke aktivert WebAuthn ennå
|
||||
not_supported: Denne nettleseren støtter ikke sikkerhetsnøkler
|
||||
otp_required: For å bruke sikkerhetsnøkler, må du først aktivere to-faktor autentisering.
|
||||
registered_on: Registrert den %{date}
|
||||
|
|
|
|||
|
|
@ -2214,5 +2214,4 @@ pl:
|
|||
nickname_hint: Wprowadź nazwę twojego nowego klucza bezpieczeństwa
|
||||
not_enabled: Nie włączyłeś WebAuthn
|
||||
not_supported: Twoja przeglądarka nie obsługuje kluczy bezpieczeństwa
|
||||
otp_required: Aby użyć kluczy bezpieczeństwa, najpierw włącz uwierzytelnianie dwuskładnikowe.
|
||||
registered_on: Zarejestrowano %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ pt-BR:
|
|||
nickname_hint: Digite o apelido da sua nova chave de segurança
|
||||
not_enabled: Você ainda não habilitou o WebAuthn
|
||||
not_supported: Este navegador não tem suporte a chaves de segurança
|
||||
otp_required: Para usar chaves de segurança, ative a autenticação de dois fatores.
|
||||
registered_on: Registrado em %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ pt-PT:
|
|||
nickname_hint: Introduz a alcunha da tua nova chave de segurança
|
||||
not_enabled: Ainda não ativaste o WebAuthn
|
||||
not_supported: Este navegador não funciona com chaves de segurança
|
||||
otp_required: Para utilizares chaves de segurança, ativa primeiro a autenticação de dois fatores.
|
||||
registered_on: Registado em %{date}
|
||||
|
|
|
|||
|
|
@ -2201,5 +2201,4 @@ ru:
|
|||
nickname_hint: Введите название для нового электронного ключа
|
||||
not_enabled: Вы еще не включили WebAuthn
|
||||
not_supported: В этом браузере отсутствует поддержка электронных ключей
|
||||
otp_required: Чтобы использовать электронные ключи, сначала включите двухфакторную аутентификацию.
|
||||
registered_on: Зарегистрирован %{date}
|
||||
|
|
|
|||
|
|
@ -1258,5 +1258,4 @@ sc:
|
|||
nickname_hint: Inserta su nomìngiu de sa crae de seguresa tua noa
|
||||
not_enabled: No as ativadu ancora WebAuthn
|
||||
not_supported: Custu navigadore no est cumpatìbile cun is craes de seguresa
|
||||
otp_required: Pro impreare is craes de seguresa depes ativare prima s'autenticatzione in duos passos.
|
||||
registered_on: 'Registratzione: %{date}'
|
||||
|
|
|
|||
|
|
@ -1532,5 +1532,4 @@ sco:
|
|||
nickname_hint: Pit in the nickname o yer new security key
|
||||
not_enabled: Ye huvnae turnt on WebAuthn yit
|
||||
not_supported: This brooser disnae support security keys
|
||||
otp_required: Fir tae uise security keys please turn on twa-factor authentication furst.
|
||||
registered_on: Registert on %{date}
|
||||
|
|
|
|||
|
|
@ -1396,5 +1396,4 @@ si:
|
|||
nickname_hint: ඔබගේ නව ආරක්ෂක යතුරේ අන්වර්ථ නාමය ඇතුළත් කරන්න
|
||||
not_enabled: ඔබ තවමත් WebAuthn සබල කර නැත
|
||||
not_supported: මෙම බ්රවුසරය ආරක්ෂක යතුරු සඳහා සහය නොදක්වයි
|
||||
otp_required: ආරක්ෂක යතුරු භාවිතා කිරීමට කරුණාකර පළමුව ද්වි-සාධක සත්යාපනය සක්රීය කරන්න.
|
||||
registered_on: "%{date} දී ලියාපදිංචි වී ඇත"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2196,5 +2196,4 @@ sl:
|
|||
nickname_hint: Vnesite vzdevek svojega novega varnostnega ključa
|
||||
not_enabled: Niste še omogočili WebAuthn
|
||||
not_supported: Ta brskalnik ne podpira varnostnih ključev
|
||||
otp_required: Za uporabo varnostnih ključev morate najprej omogočiti 2FA (dvostopenjsko overjanje).
|
||||
registered_on: Datum registracije %{date}
|
||||
|
|
|
|||
|
|
@ -2161,5 +2161,4 @@ sq:
|
|||
nickname_hint: Jepni nofkën e kyçit tuaj të ri të sigurisë
|
||||
not_enabled: S’e keni aktivizuar ende WebAuthn-in
|
||||
not_supported: Ky shfletues nuk mbulon kyçe sigurie
|
||||
otp_required: Që të përdoren kyçe sigurie, ju lutemi, së pari aktivizoni mirëfilltësimin dyfaktorësh.
|
||||
registered_on: Regjistruar më %{date}
|
||||
|
|
|
|||
|
|
@ -1854,5 +1854,4 @@ sr-Latn:
|
|||
nickname_hint: Unesite nadimak svog novog sigurnosnog ključa
|
||||
not_enabled: Još uvek niste omogućili WebAuthn
|
||||
not_supported: Ovaj pretraživač ne podržava sigurnosne ključeve
|
||||
otp_required: Da biste koristili sigurnosne ključeve, molimo Vas prvo uključite dvofaktorsku autentifikaciju.
|
||||
registered_on: Registrovan/-a %{date}
|
||||
|
|
|
|||
|
|
@ -1884,5 +1884,4 @@ sr:
|
|||
nickname_hint: Унесите надимак свог новог сигурносног кључа
|
||||
not_enabled: Још увек нисте омогућили WebAuthn
|
||||
not_supported: Овај претраживач не подржава сигурносне кључеве
|
||||
otp_required: Да бисте користили сигурносне кључеве, молимо Вас прво укључите двофакторску аутентификацију.
|
||||
registered_on: Регистрован/-а %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ sv:
|
|||
nickname_hint: Ange smeknamnet på din nya säkerhetsnyckel
|
||||
not_enabled: Du har inte aktiverat WebAuthn än
|
||||
not_supported: Denna webbläsare stöder inte säkerhetsnycklar
|
||||
otp_required: För att använda säkerhetsnycklar måste du först aktivera tvåfaktorsautentisering.
|
||||
registered_on: Registrerad den %{date}
|
||||
|
|
|
|||
|
|
@ -2010,5 +2010,4 @@ th:
|
|||
nickname_hint: ป้อนชื่อเล่นของกุญแจความปลอดภัยใหม่ของคุณ
|
||||
not_enabled: คุณยังไม่ได้เปิดใช้งาน WebAuthn
|
||||
not_supported: เบราว์เซอร์นี้ไม่รองรับกุญแจความปลอดภัย
|
||||
otp_required: เพื่อใช้กุญแจความปลอดภัย โปรดเปิดใช้งานการรับรองความถูกต้องด้วยสองปัจจัยก่อน
|
||||
registered_on: ลงทะเบียนเมื่อ %{date}
|
||||
|
|
|
|||
|
|
@ -2177,5 +2177,4 @@ tr:
|
|||
nickname_hint: Yeni güvenlik anahtarınızın takma adını girin
|
||||
not_enabled: Henüz WebAuthn'u etkinleştirmediniz
|
||||
not_supported: Bu tarayıcı güvenlik anahtarlarını desteklemiyor
|
||||
otp_required: Güvenlik anahtarlarını kullanmak için lütfen önce iki adımlı kimlik doğrulamayı etkinleştirin.
|
||||
registered_on: "%{date} tarihinde kaydoldu"
|
||||
|
|
|
|||
|
|
@ -2103,5 +2103,4 @@ uk:
|
|||
nickname_hint: Введіть псевдонім нового ключа безпеки
|
||||
not_enabled: Ви ще не активували WebAuthn
|
||||
not_supported: Цей браузер не підтримує ключі безпеки
|
||||
otp_required: Для використання ключів безпеки, спочатку увімкніть двофакторну аутентифікацію.
|
||||
registered_on: Зареєстровано %{date}
|
||||
|
|
|
|||
|
|
@ -2133,5 +2133,4 @@ vi:
|
|||
nickname_hint: Nhập tên mới cho khóa bảo mật của bạn
|
||||
not_enabled: Bạn chưa kích hoạt WebAuthn
|
||||
not_supported: Trình duyệt của bạn không hỗ trợ khóa bảo mật
|
||||
otp_required: Để dùng khóa bảo mật, trước tiên hãy kích hoạt xác thực 2 bước.
|
||||
registered_on: Đăng ký vào %{date}
|
||||
|
|
|
|||
|
|
@ -2133,5 +2133,4 @@ zh-CN:
|
|||
nickname_hint: 输入你的新安全密钥的昵称
|
||||
not_enabled: 你尚未启用WebAuthn
|
||||
not_supported: 此浏览器不支持安全密钥
|
||||
otp_required: 要使用安全密钥,请先启用双因素认证。
|
||||
registered_on: 注册于 %{date}
|
||||
|
|
|
|||
|
|
@ -1834,5 +1834,4 @@ zh-HK:
|
|||
nickname_hint: 請為你的安全密鑰裝置命名
|
||||
not_enabled: 你還未啟用 WebAuthn
|
||||
not_supported: 這個瀏覽器並不支援安全密鑰裝置
|
||||
otp_required: 請開啟雙重認證以使用安全密鑰裝置
|
||||
registered_on: 在 %{date} 注冊
|
||||
|
|
|
|||
|
|
@ -2139,5 +2139,4 @@ zh-TW:
|
|||
nickname_hint: 輸入您新安全金鑰的暱稱
|
||||
not_enabled: 您尚未啟用 WebAuthn
|
||||
not_supported: 此瀏覽器並不支援安全金鑰
|
||||
otp_required: 請先啟用兩階段驗證以使用安全金鑰。
|
||||
registered_on: 註冊於 %{date}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace :settings do
|
|||
end
|
||||
|
||||
scope module: :two_factor_authentication do
|
||||
resource :otp_authentication, only: [:show, :create], controller: :otp_authentication
|
||||
resource :otp_authentication, only: [:show, :create, :destroy], controller: :otp_authentication
|
||||
|
||||
resources :webauthn_credentials, only: [:index, :new, :create, :destroy], path: 'security_keys' do
|
||||
collection do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -349,9 +349,9 @@ RSpec.describe Auth::SessionsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with WebAuthn and OTP enabled as second factor' do
|
||||
context 'with WebAuthn enabled as second factor' do
|
||||
let!(:user) do
|
||||
Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret)
|
||||
Fabricate(:user, email: 'x@y.com', password: 'abcdefgh')
|
||||
end
|
||||
|
||||
let!(:webauthn_credential) do
|
||||
|
|
|
|||
|
|
@ -96,4 +96,26 @@ RSpec.describe Settings::TwoFactorAuthentication::OtpAuthenticationController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #destroy' do
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
it 'redirects to two factor authentication methods list page' do
|
||||
delete :destroy
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'redirects to login' do
|
||||
delete :destroy
|
||||
|
||||
expect(response).to redirect_to new_user_session_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,29 +20,10 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
it 'returns http success' do
|
||||
get :new
|
||||
|
||||
it 'returns http success' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
get :new
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -53,40 +34,21 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
|
||||
it 'returns http success' do
|
||||
get :index
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not has webauthn enabled' do
|
||||
it 'redirects to 2FA methods list page' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
context 'when user does not has webauthn enabled' do
|
||||
it 'redirects to 2FA methods list page' do
|
||||
get :index
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
|
|
@ -110,50 +72,53 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
it 'returns http success' do
|
||||
get :options
|
||||
|
||||
it 'includes existing credentials in list of excluded credentials', :aggregate_failures do
|
||||
expect { get :options }.to_not change(user, :webauthn_id)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
|
||||
excluded_credentials_ids = response.parsed_body['excludeCredentials'].pluck('id')
|
||||
expect(excluded_credentials_ids).to match_array(user.webauthn_credentials.pluck(:external_id))
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'stores the challenge on the session and sets user webauthn_id', :aggregate_failures do
|
||||
get :options
|
||||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
expect(user.reload.webauthn_id).to be_present
|
||||
end
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'does not change webauthn_id' do
|
||||
expect { get :options }.to_not change(user, :webauthn_id)
|
||||
end
|
||||
|
||||
it 'includes existing credentials in list of excluded credentials' do
|
||||
get :options
|
||||
|
||||
excluded_credentials_ids = response.parsed_body['excludeCredentials'].pluck('id')
|
||||
expect(excluded_credentials_ids).to match_array(user.webauthn_credentials.pluck(:external_id))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has not enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'returns http success' do
|
||||
get :options
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'stores the challenge on the session' do
|
||||
get :options
|
||||
|
||||
expect(controller.session[:webauthn_challenge]).to be_present
|
||||
end
|
||||
|
||||
it 'sets user webauthn_id' do
|
||||
get :options
|
||||
|
||||
expect(user.reload.webauthn_id).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -183,29 +148,40 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has enabled otp' do
|
||||
context 'when user has enabled webauthn' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when user has enabled webauthn' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
context 'when creation succeeds' do
|
||||
it 'returns http success' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'adds a new credential to user credentials and does not change webauthn_id when creation succeeds', :aggregate_failures do
|
||||
it 'adds a new credential to user credentials' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to change { user.webauthn_credentials.count }.by(1)
|
||||
.and not_change(user, :webauthn_id)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'fails when the nickname is already used' do
|
||||
it 'does not change webauthn_id' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to_not change(user, :webauthn_id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the nickname is already used' do
|
||||
it 'fails' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
||||
|
|
@ -213,14 +189,19 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
expect(response).to have_http_status(422)
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
it 'fails when the credential already exists' do
|
||||
context 'when the credential already exists' do
|
||||
before do
|
||||
user2 = Fabricate(:user)
|
||||
public_key_credential = WebAuthn::Credential.from_create(new_webauthn_credential)
|
||||
Fabricate(:webauthn_credential,
|
||||
user_id: Fabricate(:user).id,
|
||||
user_id: user2.id,
|
||||
external_id: public_key_credential.id,
|
||||
public_key: public_key_credential.public_key)
|
||||
end
|
||||
|
||||
it 'fails' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
|
@ -230,29 +211,18 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user have not enabled webauthn and creation succeeds' do
|
||||
it 'creates a webauthn credential' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
context 'when user have not enabled webauthn' do
|
||||
context 'when creation succeeds' do
|
||||
it 'creates a webauthn credential' do
|
||||
controller.session[:webauthn_challenge] = challenge
|
||||
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to change { user.webauthn_credentials.count }.by(1)
|
||||
expect do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
end.to change { user.webauthn_credentials.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has not enabled otp' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'requires otp enabled first' do
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
|
|
@ -270,39 +240,30 @@ RSpec.describe Settings::TwoFactorAuthentication::WebauthnCredentialsController
|
|||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
context 'when user has otp enabled' do
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
context 'when user has webauthn enabled' do
|
||||
before do
|
||||
user.update(webauthn_id: WebAuthn.generate_user_id)
|
||||
add_webauthn_credential(user)
|
||||
end
|
||||
|
||||
it 'redirects to 2FA methods list and shows flash success and deletes the credential when deletion succeeds', :aggregate_failures do
|
||||
expect do
|
||||
delete :destroy, params: { id: user.webauthn_credentials.take.id }
|
||||
end.to change { user.webauthn_credentials.count }.by(-1)
|
||||
context 'when deletion succeeds' do
|
||||
it 'redirects to 2FA methods list and shows flash success' do
|
||||
delete :destroy, params: { id: user.webauthn_credentials.take.id }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:success]).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'redirects to 2FA methods list and shows flash error' do
|
||||
delete :destroy, params: { id: '1' }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
expect(flash[:error]).to be_present
|
||||
it 'deletes the credential' do
|
||||
expect do
|
||||
delete :destroy, params: { id: user.webauthn_credentials.take.id }
|
||||
end.to change { user.webauthn_credentials.count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have otp enabled' do
|
||||
it 'requires otp enabled first' do
|
||||
context 'when user does not have webauthn enabled' do
|
||||
it 'redirects to 2FA methods list and shows flash error' do
|
||||
delete :destroy, params: { id: '1' }
|
||||
|
||||
expect(response).to redirect_to settings_two_factor_authentication_methods_path
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,52 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#disable_otp_login!' do
|
||||
describe 'when user has OTP enabled' do
|
||||
let(:user) do
|
||||
Fabricate(
|
||||
:user,
|
||||
otp_required_for_login: true,
|
||||
otp_secret: 'oldotpcode'
|
||||
)
|
||||
end
|
||||
|
||||
it 'saves false for otp_required_for_login' do
|
||||
user.disable_otp_login!
|
||||
|
||||
expect(user.reload.otp_required_for_login).to be false
|
||||
end
|
||||
|
||||
it 'saves nil for otp_secret' do
|
||||
user.disable_otp_login!
|
||||
|
||||
expect(user.reload.otp_secret).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when user does not have OTP enabled' do
|
||||
let(:user) do
|
||||
Fabricate(
|
||||
:user,
|
||||
otp_required_for_login: false,
|
||||
otp_secret: nil
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not change for otp_required_for_login' do
|
||||
user.disable_otp_login!
|
||||
|
||||
expect(user.reload.otp_required_for_login).to be false
|
||||
end
|
||||
|
||||
it 'does not change for otp_secret' do
|
||||
user.disable_otp_login!
|
||||
|
||||
expect(user.reload.otp_secret).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#disable_two_factor!' do
|
||||
it 'saves false for otp_required_for_login' do
|
||||
user = Fabricate.build(:user, otp_required_for_login: true)
|
||||
|
|
|
|||
|
|
@ -13,23 +13,4 @@ RSpec.describe 'Settings TwoFactorAuthenticationMethods' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
describe 'GET to /settings/two_factor_authentication_methods' do
|
||||
describe 'when user has not enabled otp' do
|
||||
before { user.update(otp_required_for_login: false) }
|
||||
|
||||
it 'redirects to enable otp' do
|
||||
get settings_two_factor_authentication_methods_path
|
||||
|
||||
expect(response)
|
||||
.to redirect_to(settings_otp_authentication_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,15 +26,14 @@ RSpec.describe 'Admin Users TwoFactorAuthentications' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user has OTP and WebAuthn enabled' do
|
||||
before { user.update(otp_required_for_login: true, webauthn_id: WebAuthn.generate_user_id) }
|
||||
context 'when user has WebAuthn enabled' do
|
||||
before { user.update(webauthn_id: WebAuthn.generate_user_id) }
|
||||
|
||||
it 'disables OTP and webauthn and redirects to admin account page' do
|
||||
visit admin_account_path(user.account.id)
|
||||
|
||||
expect { disable_two_factor }
|
||||
.to change { user.reload.otp_enabled? }.to(false)
|
||||
.and(change { user.reload.webauthn_enabled? }.to(false))
|
||||
.to change { user.reload.webauthn_enabled? }.to(false)
|
||||
expect(page)
|
||||
.to have_title(user.account.pretty_acct)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user