Merge pull request #3268 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 58b3fc03796dbde54dbba9a4c554c2d8d24f81fd
This commit is contained in:
Claire 2025-11-06 13:09:22 +01:00 committed by GitHub
commit 1a890d2077
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
186 changed files with 1247 additions and 741 deletions

View File

@ -23,8 +23,6 @@
// Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'],
matchPackageNames: [
'tesseract.js', // Requires code changes
// react-router: Requires manual upgrade
'history',
'react-router-dom',

View File

@ -6,13 +6,14 @@ All notable changes to this project will be documented in this file.
### Added
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550 and #36559 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721 and #36695 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, and #36607 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds, with 3 values each: `public`, `authenticated`, `disabled`.\
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
When `disabled`, users with the “View live and topic feeds” will still be able to view them.
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
@ -43,6 +44,9 @@ All notable changes to this project will be documented in this file.
- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change styling of column banners (#36531 by @ClearlyClaire)
- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
@ -70,9 +74,11 @@ All notable changes to this project will be documented in this file.
- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
- Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672 by @diondiondion)
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)

View File

@ -13,7 +13,7 @@ gem 'haml-rails', '~>3.0'
gem 'pg', '~> 1.5'
gem 'pghero'
gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
gem 'aws-sdk-core', require: false
gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.6.0'

View File

@ -96,17 +96,20 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1168.0)
aws-sdk-core (3.215.1)
aws-partitions (1.1180.0)
aws-sdk-core (3.236.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
logger
aws-sdk-kms (1.116.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.202.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@ -933,7 +936,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotaterb (~> 4.13)
aws-sdk-core (< 3.216.0)
aws-sdk-core
aws-sdk-s3 (~> 1.123)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)

View File

@ -49,7 +49,11 @@ export const Alert: React.FC<{
</span>
{hasAction && (
<button className='notification-bar__action' onClick={onActionClick}>
<button
className='notification-bar__action'
onClick={onActionClick}
type='button'
>
{action}
</button>
)}

View File

@ -78,6 +78,7 @@ export const Button: React.FC<Props> = ({
aria-live={loading !== undefined ? 'polite' : undefined}
onClick={handleClick}
title={title}
// eslint-disable-next-line react/button-has-type -- set correctly via TS
type={type}
{...props}
>

View File

@ -30,7 +30,7 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
const handleClick = useHandleClick(onClick);
const component = (
<button onClick={handleClick} className='column-back-button'>
<button onClick={handleClick} className='column-back-button' type='button'>
<Icon
id='chevron-left'
icon={ArrowBackIcon}

View File

@ -53,6 +53,7 @@ const BackButton: React.FC<{
compact: onlyIcon,
})}
aria-label={intl.formatMessage(messages.back)}
type='button'
>
<Icon
id='chevron-left'
@ -172,6 +173,7 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='times' icon={CloseIcon} />{' '}
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
@ -185,6 +187,7 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveLeft)}
className='icon-button column-header__setting-btn'
onClick={handleMoveLeft}
type='button'
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
@ -193,6 +196,7 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveRight)}
className='icon-button column-header__setting-btn'
onClick={handleMoveRight}
type='button'
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
@ -203,6 +207,7 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='plus' icon={AddIcon} />{' '}
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
@ -237,6 +242,7 @@ export const ColumnHeader: React.FC<Props> = ({
collapsed ? messages.show : messages.hide,
)}
onClick={handleToggleClick}
type='button'
>
<i className='icon-with-badge'>
<Icon
@ -259,7 +265,11 @@ export const ColumnHeader: React.FC<Props> = ({
<>
{backButton}
<button onClick={handleTitleClick} className='column-header__title'>
<button
onClick={handleTitleClick}
className='column-header__title'
type='button'
>
{!backButton && (
<Icon
id={icon}

View File

@ -74,7 +74,7 @@ export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
onBlur={handleBlur}
/>
<button className='button' onClick={handleButtonClick}>
<button className='button' onClick={handleButtonClick} type='button'>
<Icon id='copy' icon={ContentCopyIcon} />{' '}
{copied ? (
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />

View File

@ -237,6 +237,7 @@ export const DropdownMenu = <Item = MenuItem,>({
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
type='button'
>
<DropdownMenuItemContent item={option} />
</button>

View File

@ -98,7 +98,12 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
<button
data-index={index}
onClick={onClick}
onKeyUp={onKeyUp}
type='button'
>
{label}
</button>
</li>
@ -118,7 +123,7 @@ export const EditedTimestamp: React.FC<{
onItemClick={handleItemClick}
forceDropdown
>
<button className='dropdown-menu__text-button'>
<button className='dropdown-menu__text-button' type='button'>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'

View File

@ -235,7 +235,7 @@ const HashtagBar: React.FC<{
))}
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
<button className='link-button' onClick={handleClick}>
<button className='link-button' onClick={handleClick} type='button'>
<FormattedMessage
id='hashtags.and_other'
defaultMessage='…and {count, plural, other {# more}}'

View File

@ -23,6 +23,7 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
<FormattedMessage
id='learn_more_link.learn_more'
@ -48,7 +49,11 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
<div className='learn-more__popout__content'>{children}</div>
<div>
<button className='link-button' onClick={handleClick}>
<button
className='link-button'
onClick={handleClick}
type='button'
>
<FormattedMessage
id='learn_more_link.got_it'
defaultMessage='Got it'

View File

@ -32,6 +32,7 @@ export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
onClick={handleClick}
aria-label={intl.formatMessage(messages.load_more)}
title={intl.formatMessage(messages.load_more)}
type='button'
>
{loading ? (
<LoadingIndicator />

View File

@ -7,7 +7,7 @@ interface Props {
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
return (
<button className='load-more load-gap' onClick={onClick}>
<button className='load-more load-gap' onClick={onClick} type='button'>
<FormattedMessage
id='load_pending'
defaultMessage='{count, plural, one {# new item} other {# new items}}'

View File

@ -171,13 +171,14 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
className='button button-secondary'
disabled={voteDisabled}
onClick={handleVote}
type='button'
>
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
</button>
)}
{!showResults && (
<>
<button className='poll__link' onClick={handleReveal}>
<button className='poll__link' onClick={handleReveal} type='button'>
<FormattedMessage id='poll.reveal' defaultMessage='See results' />
</button>{' '}
·{' '}
@ -185,7 +186,11 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
)}
{showResults && !disabled && (
<>
<button className='poll__link' onClick={handleRefresh}>
<button
className='poll__link'
onClick={handleRefresh}
type='button'
>
<FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
</button>{' '}
·{' '}

View File

@ -232,6 +232,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
ref={focusRefCallback}
aria-disabled={disabled}
data-index={index}
type='button'
>
<DropdownMenuItemContent item={item} />
</button>

View File

@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
<button
ref={buttonRef}
type='button'
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}

View File

@ -73,7 +73,7 @@ const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
defaultMessage='This account has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
<button onClick={reveal} className='link-button'>
<button onClick={reveal} className='link-button' type='button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'
@ -129,7 +129,7 @@ const FilteredQuote: React.FC<{
return (
<>
{message}
<button onClick={reveal} className='link-button'>
<button onClick={reveal} className='link-button' type='button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'

View File

@ -31,6 +31,7 @@ export const Section: FC<SectionProps> = ({
className='about__section__title'
tabIndex={0}
onClick={handleClick}
type='button'
>
<Icon
id={collapsed ? 'chevron-right' : 'chevron-down'}

View File

@ -37,6 +37,7 @@ export const DomainPill: React.FC<{
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
{domain}
</button>
@ -154,6 +155,7 @@ export const DomainPill: React.FC<{
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>
@ -169,6 +171,7 @@ export const DomainPill: React.FC<{
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>

View File

@ -489,6 +489,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
className='link-button'
onClick={handleDetectClick}
disabled={type !== 'image' || isDetecting}
type='button'
>
<FormattedMessage
id='alt_text_modal.add_text_from_image'

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState, useId } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@ -22,6 +22,8 @@ import { useAudioVisualizer } from 'flavours/glitch/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { playerSettings } from 'flavours/glitch/settings';
import { AudioVisualizer } from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
@ -116,7 +118,6 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
@ -538,19 +539,6 @@ export const Audio: React.FC<{
[togglePlay, toggleMute],
);
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
@ -641,81 +629,7 @@ export const Audio: React.FC<{
</div>
<div className='audio-player__controls__play'>
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect
x={14}
y={14}
width={96}
height={96}
rx={48}
fill='white'
/>
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
<AudioVisualizer frequencyBands={frequencyBands} poster={poster} />
<button
type='button'

View File

@ -0,0 +1,100 @@
import { useId } from 'react';
import type { FC } from 'react';
import { animated, config, useSpring } from '@react-spring/web';
interface AudioVisualizerProps {
frequencyBands?: number[];
poster?: string;
}
export const AudioVisualizer: FC<AudioVisualizerProps> = ({
frequencyBands = [],
poster,
}) => {
const accessibilityId = useId();
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
return (
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect x={14} y={14} width={96} height={96} rx={48} fill='white' />
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
);
};

View File

@ -557,7 +557,11 @@ export const Search: React.FC<{
)}
>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}>
<button
className='icon-button'
onMouseDown={forget}
type='button'
>
<Icon id='times' icon={CloseIcon} />
</button>
</div>
@ -591,6 +595,7 @@ export const Search: React.FC<{
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
type='button'
>
{label}
</button>
@ -617,6 +622,7 @@ export const Search: React.FC<{
selectedOption ===
(quickActions.length || recent.length) + i,
})}
type='button'
>
{label}
</button>

View File

@ -10,6 +10,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import SoundIcon from '@/material-icons/400-24px/audio.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose } from 'flavours/glitch/actions/compose';
@ -17,7 +18,18 @@ import { openModal } from 'flavours/glitch/actions/modal';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from 'flavours/glitch/store';
import { AudioVisualizer } from '../../audio/visualizer';
const selectUserAvatar = createAppSelector(
[(state) => state.accounts, (state) => state.meta.get('me') as string],
(accounts, myId) => accounts.get(myId)?.avatar_static,
);
export const Upload: React.FC<{
id: string;
@ -38,6 +50,7 @@ export const Upload: React.FC<{
const sensitive = useAppSelector(
(state) => state.compose.get('sensitive') as boolean,
);
const userAvatar = useAppSelector(selectUserAvatar);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
@ -67,6 +80,8 @@ export const Upload: React.FC<{
transform: CSS.Transform.toString(transform),
transition,
};
const preview_url = media.get('preview_url') as string | null;
const blurhash = media.get('blurhash') as string | null;
return (
<div
@ -85,17 +100,19 @@ export const Upload: React.FC<{
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundImage:
!sensitive && preview_url ? `url(${preview_url})` : undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
{sensitive && blurhash && (
<Blurhash hash={blurhash} className='compose-form__upload__preview' />
)}
{!sensitive && !preview_url && (
<div className='compose-form__upload__visualizer'>
<AudioVisualizer poster={userAvatar} />
<Icon id='sound' icon={SoundIcon} />
</div>
)}
<div className='compose-form__upload__actions'>

View File

@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { dismissAnnouncement } from '@/flavours/glitch/actions/announcements';
import type { ApiAnnouncementJSON } from '@/flavours/glitch/api_types/announcements';
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
import { useAppDispatch } from '@/flavours/glitch/store';
import { ReactionsBar } from './reactions';
@ -22,13 +24,23 @@ export const Announcement: FC<AnnouncementProps> = ({
announcement,
active,
}) => {
const [unread, setUnread] = useState(!announcement.read);
const { read, id } = announcement;
// Dismiss announcement when it becomes active.
const dispatch = useAppDispatch();
useEffect(() => {
// Only update `unread` marker once the announcement is out of view
if (!active && unread !== !announcement.read) {
setUnread(!announcement.read);
if (active && !read) {
dispatch(dismissAnnouncement(id));
}
}, [announcement.read, active, unread]);
}, [active, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!active && unread !== !read) {
setUnread(!read);
}
}, [active, unread, read]);
return (
<AnimateEmojiProvider>

View File

@ -270,7 +270,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
</h3>
<div className='inline-follow-suggestions__header__actions'>
<button className='link-button' onClick={handleDismiss}>
<button className='link-button' onClick={handleDismiss} type='button'>
<FormattedMessage
id='follow_suggestions.dismiss'
defaultMessage="Don't show again"
@ -309,6 +309,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
className='inline-follow-suggestions__body__scroll-button left'
onClick={handleLeftNav}
aria-label={intl.formatMessage(messages.previous)}
type='button'
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronLeftIcon} />
@ -321,6 +322,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
className='inline-follow-suggestions__body__scroll-button right'
onClick={handleRightNav}
aria-label={intl.formatMessage(messages.next)}
type='button'
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronRightIcon} />

View File

@ -381,6 +381,7 @@ const LoginForm: React.FC<{
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
type='button'
>
{option
.split(domainRegExp)
@ -451,7 +452,7 @@ const InteractionModal: React.FC<{
);
} else {
signupButton = (
<button className='link-button' onClick={handleSignupClick}>
<button className='link-button' onClick={handleSignupClick} type='button'>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'

View File

@ -22,6 +22,7 @@ import { Column } from 'flavours/glitch/components/column';
import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import type { List } from 'flavours/glitch/models/list';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { messages as membersMessages } from './members';
@ -72,36 +73,23 @@ const MembersLink: React.FC<{
);
};
const NewList: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const intl = useIntl();
const history = useHistory();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
const [title, setTitle] = useState('');
const [exclusive, setExclusive] = useState(false);
const [repliesPolicy, setRepliesPolicy] = useState<RepliesPolicyType>('list');
const {
id,
title: initialTitle = '',
exclusive: initialExclusive = false,
replies_policy: initialRepliesPolicy = 'list',
} = list ?? {};
const [title, setTitle] = useState(initialTitle);
const [exclusive, setExclusive] = useState(initialExclusive);
const [repliesPolicy, setRepliesPolicy] =
useState<RepliesPolicyType>(initialRepliesPolicy);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
useEffect(() => {
if (id && list) {
setTitle(list.title);
setExclusive(list.exclusive);
setRepliesPolicy(list.replies_policy);
}
}, [setTitle, setExclusive, setRepliesPolicy, id, list]);
const handleTitleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setTitle(value);
@ -158,6 +146,138 @@ const NewList: React.FC<{
}
}, [history, dispatch, setSubmitting, id, title, exclusive, repliesPolicy]);
return (
<form className='simple_form app-form' onSubmit={handleSubmit}>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_title'>
<FormattedMessage
id='lists.list_name'
defaultMessage='List name'
/>
</label>
<div className='label_input__wrapper'>
<input
id='list_title'
type='text'
value={title}
onChange={handleTitleChange}
maxLength={30}
required
placeholder=' '
/>
</div>
</div>
</div>
</div>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_replies_policy'>
<FormattedMessage
id='lists.show_replies_to'
defaultMessage='Include replies from list members to'
/>
</label>
<div className='label_input__wrapper'>
<select
id='list_replies_policy'
value={repliesPolicy}
onChange={handleRepliesPolicyChange}
>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
>
{(msg) => <option value='none'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.list'
defaultMessage='Members of the list'
>
{(msg) => <option value='list'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
>
{(msg) => <option value='followed'>{msg}</option>}
</FormattedMessage>
</select>
</div>
</div>
</div>
</div>
{id && (
<div className='fields-group'>
<MembersLink id={id} />
</div>
)}
<div className='fields-group'>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong>
<FormattedMessage
id='lists.exclusive'
defaultMessage='Hide members in Home'
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.exclusive_hint'
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={exclusive} onChange={handleExclusiveChange} />
</div>
</div>
</label>
</div>
<div className='actions'>
<button className='button' type='submit'>
{submitting ? (
<LoadingIndicator />
) : id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage id='lists.create' defaultMessage='Create' />
)}
</button>
</div>
</form>
);
};
const NewListWrapper: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
useEffect(() => {
if (id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
const isLoading = id && !list;
return (
<Column
bindToDocument={!multiColumn}
@ -172,119 +292,7 @@ const NewList: React.FC<{
/>
<div className='scrollable'>
<form className='simple_form app-form' onSubmit={handleSubmit}>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_title'>
<FormattedMessage
id='lists.list_name'
defaultMessage='List name'
/>
</label>
<div className='label_input__wrapper'>
<input
id='list_title'
type='text'
value={title}
onChange={handleTitleChange}
maxLength={30}
required
placeholder=' '
/>
</div>
</div>
</div>
</div>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_replies_policy'>
<FormattedMessage
id='lists.show_replies_to'
defaultMessage='Include replies from list members to'
/>
</label>
<div className='label_input__wrapper'>
<select
id='list_replies_policy'
value={repliesPolicy}
onChange={handleRepliesPolicyChange}
>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
>
{(msg) => <option value='none'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.list'
defaultMessage='Members of the list'
>
{(msg) => <option value='list'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
>
{(msg) => <option value='followed'>{msg}</option>}
</FormattedMessage>
</select>
</div>
</div>
</div>
</div>
{id && (
<div className='fields-group'>
<MembersLink id={id} />
</div>
)}
<div className='fields-group'>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong>
<FormattedMessage
id='lists.exclusive'
defaultMessage='Hide members in Home'
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.exclusive_hint'
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle
checked={exclusive}
onChange={handleExclusiveChange}
/>
</div>
</div>
</label>
</div>
<div className='actions'>
<button className='button' type='submit'>
{submitting ? (
<LoadingIndicator />
) : id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage id='lists.create' defaultMessage='Create' />
)}
</button>
</div>
</form>
{isLoading ? <LoadingIndicator /> : <NewList list={list} />}
</div>
<Helmet>
@ -298,4 +306,4 @@ const NewList: React.FC<{
};
// eslint-disable-next-line import/no-default-export
export default NewList;
export default NewListWrapper;

View File

@ -116,7 +116,7 @@ export const MoreLink: React.FC = () => {
return (
<Dropdown items={menu} placement='bottom-start'>
<button className='column-link column-link--transparent'>
<button className='column-link column-link--transparent' type='button'>
<Icon id='' icon={MoreHorizIcon} className='column-link__icon' />
<FormattedMessage id='navigation_bar.more' defaultMessage='More' />

View File

@ -71,6 +71,7 @@ export const SignInBanner: React.FC = () => {
<button
className='button button--block'
onClick={openClosedRegistrationsModal}
type='button'
>
<FormattedMessage
id='sign_in_banner.create_account'

View File

@ -45,6 +45,7 @@ export const FilteredNotificationsIconButton: React.FC<{
title={intl.formatMessage(messages.filteredNotifications)}
onClick={handleClick}
className={className}
type='button'
>
<Icon id='filtered-notifications' icon={InventoryIcon} />
</button>

View File

@ -47,7 +47,7 @@ export const NotificationAnnualReport: React.FC<{
values={{ year }}
/>
</p>
<button onClick={handleClick} className='link-button'>
<button onClick={handleClick} className='link-button' type='button'>
<FormattedMessage
id='notification.annual_report.view'
defaultMessage='View #Wrapstodon'

View File

@ -51,6 +51,7 @@ const BarButton: React.FC<
className={selectedFilter === type ? 'active' : ''}
onClick={onClick}
title={title}
type='button'
>
{children}
</button>

View File

@ -245,6 +245,7 @@ export const Notifications: React.FC<{
title={intl.formatMessage(messages.markAsRead)}
onClick={handleMarkAsRead}
className='column-header__button'
type='button'
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>

View File

@ -9,7 +9,7 @@ export const SearchSection: React.FC<{
<div className='search-results__section__header'>
<h3>{title}</h3>
{onClickMore && (
<button onClick={onClickMore}>
<button onClick={onClickMore} type='button'>
<FormattedMessage
id='search_results.see_all'
defaultMessage='See all'

View File

@ -232,12 +232,14 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectAll}
className={mappedType === 'all' ? 'active' : undefined}
type='button'
>
<FormattedMessage id='search_results.all' defaultMessage='All' />
</button>
<button
onClick={handleSelectAccounts}
className={mappedType === 'accounts' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.accounts'
@ -247,6 +249,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectHashtags}
className={mappedType === 'hashtags' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.hashtags'
@ -256,6 +259,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectStatuses}
className={mappedType === 'statuses' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.statuses'

View File

@ -25,7 +25,12 @@ export const ActionsModal: React.FC<{
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i} disabled={disabled}>
<button
onClick={onClick}
data-index={i}
disabled={disabled}
type='button'
>
<DropdownMenuItemContent item={option} />
</button>
);

View File

@ -121,7 +121,7 @@ export const BoostModal: React.FC<{
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<button onClick={handleCancel} className='link-button' type='button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'

View File

@ -66,7 +66,7 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={onClose} className='link-button'>
<button onClick={onClose} className='link-button' type='button'>
{cancel ?? (
<FormattedMessage
id='confirmation_modal.cancel'
@ -78,7 +78,11 @@ export const ConfirmationModal: React.FC<
{secondary && (
<>
<div className='spacer' />
<button onClick={handleSecondary} className='link-button'>
<button
onClick={handleSecondary}
className='link-button'
type='button'
>
{secondary}
</button>
</>

View File

@ -196,7 +196,7 @@ export const DomainBlockModal: React.FC<{
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<button onClick={handleCancel} className='link-button' type='button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'

View File

@ -72,7 +72,7 @@ export const FavouriteModal: React.FC<{
<div className='safety-action-modal__actions'>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<button onClick={handleCancel} className='link-button' type='button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'

View File

@ -247,6 +247,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
className='media-modal__nav media-modal__nav--prev'
onClick={handlePrevClick}
aria-label={intl.formatMessage(messages.previous)}
type='button'
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
@ -257,6 +258,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
className='media-modal__nav media-modal__nav--next'
onClick={handleNextClick}
aria-label={intl.formatMessage(messages.next)}
type='button'
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
@ -354,6 +356,7 @@ const MediaPagination: FC<MediaPaginationProps> = ({
active: i === index,
})}
onClick={handleChangeIndex(i)}
type='button'
>
{i + 1}
</button>

View File

@ -127,7 +127,11 @@ const LoginOrSignUp: React.FC = () => {
);
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<button
className='button'
onClick={openClosedRegistrationsModal}
type='button'
>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'
@ -195,6 +199,7 @@ export const NavigationBar: React.FC = () => {
className={classNames('ui__navigation-bar__item', { active: open })}
onClick={handleClick}
aria-label={intl.formatMessage(messages.menu)}
type='button'
>
<Icon id='' icon={MenuIcon} />
</button>

View File

@ -881,6 +881,7 @@ export const Video: React.FC<{
<button
className='media-gallery__actions__pill'
onClick={toggleReveal}
type='button'
>
<FormattedMessage
id='media_gallery.hide'

View File

@ -775,16 +775,43 @@
padding: 8px;
}
&__preview {
&__preview,
&__visualizer {
position: absolute;
width: 100%;
height: 100%;
border-radius: 6px;
z-index: -1;
top: 0;
}
&__preview {
border-radius: 6px;
inset-inline-start: 0;
}
&__visualizer {
padding: 16px;
box-sizing: border-box;
.audio-player__visualizer {
margin: 0 auto;
display: block;
height: 100%;
}
.icon {
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
opacity: 0.75;
color: var(--player-foreground-color);
filter: var(--overlay-icon-shadow);
width: 48px;
height: 48px;
}
}
&__thumbnail {
width: 100%;
height: 100%;
@ -3056,6 +3083,7 @@ a.account__display-name {
height: 100%;
min-height: 100vh;
min-height: 100dvh;
padding-inline: 10px;
padding-bottom: env(safe-area-inset-bottom);
&__pane {
@ -3395,6 +3423,7 @@ a.account__display-name {
min-height: 100vh;
min-height: 100dvh;
gap: 0;
padding-inline: 0;
}
.columns-area__panels__pane--navigational {

View File

@ -49,7 +49,11 @@ export const Alert: React.FC<{
</span>
{hasAction && (
<button className='notification-bar__action' onClick={onActionClick}>
<button
className='notification-bar__action'
onClick={onActionClick}
type='button'
>
{action}
</button>
)}

View File

@ -78,6 +78,7 @@ export const Button: React.FC<Props> = ({
aria-live={loading !== undefined ? 'polite' : undefined}
onClick={handleClick}
title={title}
// eslint-disable-next-line react/button-has-type -- set correctly via TS
type={type}
{...props}
>

View File

@ -30,7 +30,7 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
const handleClick = useHandleClick(onClick);
const component = (
<button onClick={handleClick} className='column-back-button'>
<button onClick={handleClick} className='column-back-button' type='button'>
<Icon
id='chevron-left'
icon={ArrowBackIcon}

View File

@ -53,6 +53,7 @@ const BackButton: React.FC<{
compact: onlyIcon,
})}
aria-label={intl.formatMessage(messages.back)}
type='button'
>
<Icon
id='chevron-left'
@ -172,6 +173,7 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='times' icon={CloseIcon} />{' '}
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
@ -185,6 +187,7 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveLeft)}
className='icon-button column-header__setting-btn'
onClick={handleMoveLeft}
type='button'
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
@ -193,6 +196,7 @@ export const ColumnHeader: React.FC<Props> = ({
aria-label={intl.formatMessage(messages.moveRight)}
className='icon-button column-header__setting-btn'
onClick={handleMoveRight}
type='button'
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
@ -203,6 +207,7 @@ export const ColumnHeader: React.FC<Props> = ({
<button
className='text-btn column-header__setting-btn'
onClick={handlePin}
type='button'
>
<Icon id='plus' icon={AddIcon} />{' '}
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
@ -237,6 +242,7 @@ export const ColumnHeader: React.FC<Props> = ({
collapsed ? messages.show : messages.hide,
)}
onClick={handleToggleClick}
type='button'
>
<i className='icon-with-badge'>
<Icon
@ -259,7 +265,11 @@ export const ColumnHeader: React.FC<Props> = ({
<>
{backButton}
<button onClick={handleTitleClick} className='column-header__title'>
<button
onClick={handleTitleClick}
className='column-header__title'
type='button'
>
{!backButton && (
<Icon
id={icon}

View File

@ -74,7 +74,7 @@ export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
onBlur={handleBlur}
/>
<button className='button' onClick={handleButtonClick}>
<button className='button' onClick={handleButtonClick} type='button'>
<Icon id='copy' icon={ContentCopyIcon} />{' '}
{copied ? (
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />

View File

@ -237,6 +237,7 @@ export const DropdownMenu = <Item = MenuItem,>({
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
type='button'
>
<DropdownMenuItemContent item={option} />
</button>

View File

@ -98,7 +98,12 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
<button
data-index={index}
onClick={onClick}
onKeyUp={onKeyUp}
type='button'
>
{label}
</button>
</li>
@ -118,7 +123,7 @@ export const EditedTimestamp: React.FC<{
onItemClick={handleItemClick}
forceDropdown
>
<button className='dropdown-menu__text-button'>
<button className='dropdown-menu__text-button' type='button'>
<FormattedMessage
id='status.edited'
defaultMessage='Edited {date}'

View File

@ -235,7 +235,7 @@ const HashtagBar: React.FC<{
))}
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
<button className='link-button' onClick={handleClick}>
<button className='link-button' onClick={handleClick} type='button'>
<FormattedMessage
id='hashtags.and_other'
defaultMessage='…and {count, plural, other {# more}}'

View File

@ -153,7 +153,7 @@ export const Default = {
the app.
</p>
<p>
When a <button>Button</button> is focused,
When a <button type='button'>Button</button> is focused,
<kbd>Enter</kbd>
should not trigger open, but <kbd>o</kbd>
should.

View File

@ -23,6 +23,7 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
<FormattedMessage
id='learn_more_link.learn_more'
@ -48,7 +49,11 @@ export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
<div className='learn-more__popout__content'>{children}</div>
<div>
<button className='link-button' onClick={handleClick}>
<button
className='link-button'
onClick={handleClick}
type='button'
>
<FormattedMessage
id='learn_more_link.got_it'
defaultMessage='Got it'

View File

@ -32,6 +32,7 @@ export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
onClick={handleClick}
aria-label={intl.formatMessage(messages.load_more)}
title={intl.formatMessage(messages.load_more)}
type='button'
>
{loading ? (
<LoadingIndicator />

View File

@ -7,7 +7,7 @@ interface Props {
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
return (
<button className='load-more load-gap' onClick={onClick}>
<button className='load-more load-gap' onClick={onClick} type='button'>
<FormattedMessage
id='load_pending'
defaultMessage='{count, plural, one {# new item} other {# new items}}'

View File

@ -171,13 +171,14 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
className='button button-secondary'
disabled={voteDisabled}
onClick={handleVote}
type='button'
>
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
</button>
)}
{!showResults && (
<>
<button className='poll__link' onClick={handleReveal}>
<button className='poll__link' onClick={handleReveal} type='button'>
<FormattedMessage id='poll.reveal' defaultMessage='See results' />
</button>{' '}
·{' '}
@ -185,7 +186,11 @@ export const Poll: React.FC<PollProps> = ({ pollId, disabled, status }) => {
)}
{showResults && !disabled && (
<>
<button className='poll__link' onClick={handleRefresh}>
<button
className='poll__link'
onClick={handleRefresh}
type='button'
>
<FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
</button>{' '}
·{' '}

View File

@ -232,6 +232,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
ref={focusRefCallback}
aria-disabled={disabled}
data-index={index}
type='button'
>
<DropdownMenuItemContent item={item} />
</button>

View File

@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
<button
ref={buttonRef}
type='button'
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}

View File

@ -73,7 +73,7 @@ const LimitedAccountHint: React.FC<{ accountId: string }> = ({ accountId }) => {
defaultMessage='This account has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
<button onClick={reveal} className='link-button'>
<button onClick={reveal} className='link-button' type='button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'
@ -129,7 +129,7 @@ const FilteredQuote: React.FC<{
return (
<>
{message}
<button onClick={reveal} className='link-button'>
<button onClick={reveal} className='link-button' type='button'>
<FormattedMessage
id='status.quote_error.limited_account_hint.action'
defaultMessage='Show anyway'

View File

@ -31,6 +31,7 @@ export const Section: FC<SectionProps> = ({
className='about__section__title'
tabIndex={0}
onClick={handleClick}
type='button'
>
<Icon
id={collapsed ? 'chevron-right' : 'chevron-down'}

View File

@ -37,6 +37,7 @@ export const DomainPill: React.FC<{
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
type='button'
>
{domain}
</button>
@ -154,6 +155,7 @@ export const DomainPill: React.FC<{
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>
@ -169,6 +171,7 @@ export const DomainPill: React.FC<{
<button
onClick={handleExpandClick}
className='link-button'
type='button'
>
{x}
</button>

View File

@ -489,6 +489,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
className='link-button'
onClick={handleDetectClick}
disabled={type !== 'image' || isDetecting}
type='button'
>
<FormattedMessage
id='alt_text_modal.add_text_from_image'

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState, useId } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@ -22,6 +22,8 @@ import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
import { AudioVisualizer } from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
@ -116,7 +118,6 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
@ -538,19 +539,6 @@ export const Audio: React.FC<{
[togglePlay, toggleMute],
);
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
@ -641,81 +629,7 @@ export const Audio: React.FC<{
</div>
<div className='audio-player__controls__play'>
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect
x={14}
y={14}
width={96}
height={96}
rx={48}
fill='white'
/>
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
<AudioVisualizer frequencyBands={frequencyBands} poster={poster} />
<button
type='button'

View File

@ -0,0 +1,100 @@
import { useId } from 'react';
import type { FC } from 'react';
import { animated, config, useSpring } from '@react-spring/web';
interface AudioVisualizerProps {
frequencyBands?: number[];
poster?: string;
}
export const AudioVisualizer: FC<AudioVisualizerProps> = ({
frequencyBands = [],
poster,
}) => {
const accessibilityId = useId();
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
return (
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect x={14} y={14} width={96} height={96} rx={48} fill='white' />
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
);
};

View File

@ -557,7 +557,11 @@ export const Search: React.FC<{
)}
>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}>
<button
className='icon-button'
onMouseDown={forget}
type='button'
>
<Icon id='times' icon={CloseIcon} />
</button>
</div>
@ -591,6 +595,7 @@ export const Search: React.FC<{
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
type='button'
>
{label}
</button>
@ -617,6 +622,7 @@ export const Search: React.FC<{
selectedOption ===
(quickActions.length || recent.length) + i,
})}
type='button'
>
{label}
</button>

View File

@ -10,6 +10,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import SoundIcon from '@/material-icons/400-24px/audio.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose } from 'mastodon/actions/compose';
@ -17,7 +18,18 @@ import { openModal } from 'mastodon/actions/modal';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from 'mastodon/store';
import { AudioVisualizer } from '../../audio/visualizer';
const selectUserAvatar = createAppSelector(
[(state) => state.accounts, (state) => state.meta.get('me') as string],
(accounts, myId) => accounts.get(myId)?.avatar_static,
);
export const Upload: React.FC<{
id: string;
@ -38,6 +50,7 @@ export const Upload: React.FC<{
const sensitive = useAppSelector(
(state) => state.compose.get('spoiler') as boolean,
);
const userAvatar = useAppSelector(selectUserAvatar);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
@ -67,6 +80,8 @@ export const Upload: React.FC<{
transform: CSS.Transform.toString(transform),
transition,
};
const preview_url = media.get('preview_url') as string | null;
const blurhash = media.get('blurhash') as string | null;
return (
<div
@ -85,17 +100,19 @@ export const Upload: React.FC<{
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundImage:
!sensitive && preview_url ? `url(${preview_url})` : undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
{sensitive && blurhash && (
<Blurhash hash={blurhash} className='compose-form__upload__preview' />
)}
{!sensitive && !preview_url && (
<div className='compose-form__upload__visualizer'>
<AudioVisualizer poster={userAvatar} />
<Icon id='sound' icon={SoundIcon} />
</div>
)}
<div className='compose-form__upload__actions'>

View File

@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { dismissAnnouncement } from '@/mastodon/actions/announcements';
import type { ApiAnnouncementJSON } from '@/mastodon/api_types/announcements';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { useAppDispatch } from '@/mastodon/store';
import { ReactionsBar } from './reactions';
@ -22,13 +24,23 @@ export const Announcement: FC<AnnouncementProps> = ({
announcement,
active,
}) => {
const [unread, setUnread] = useState(!announcement.read);
const { read, id } = announcement;
// Dismiss announcement when it becomes active.
const dispatch = useAppDispatch();
useEffect(() => {
// Only update `unread` marker once the announcement is out of view
if (!active && unread !== !announcement.read) {
setUnread(!announcement.read);
if (active && !read) {
dispatch(dismissAnnouncement(id));
}
}, [announcement.read, active, unread]);
}, [active, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!active && unread !== !read) {
setUnread(!read);
}
}, [active, unread, read]);
return (
<AnimateEmojiProvider>

View File

@ -270,7 +270,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
</h3>
<div className='inline-follow-suggestions__header__actions'>
<button className='link-button' onClick={handleDismiss}>
<button className='link-button' onClick={handleDismiss} type='button'>
<FormattedMessage
id='follow_suggestions.dismiss'
defaultMessage="Don't show again"
@ -309,6 +309,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
className='inline-follow-suggestions__body__scroll-button left'
onClick={handleLeftNav}
aria-label={intl.formatMessage(messages.previous)}
type='button'
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronLeftIcon} />
@ -321,6 +322,7 @@ export const InlineFollowSuggestions: React.FC<{ hidden?: boolean }> = ({
className='inline-follow-suggestions__body__scroll-button right'
onClick={handleRightNav}
aria-label={intl.formatMessage(messages.next)}
type='button'
>
<div className='inline-follow-suggestions__body__scroll-button__icon'>
<Icon id='' icon={ChevronRightIcon} />

View File

@ -381,6 +381,7 @@ const LoginForm: React.FC<{
className={classNames('search__popout__menu__item', {
selected: selectedOption === i,
})}
type='button'
>
{option
.split(domainRegExp)
@ -451,7 +452,7 @@ const InteractionModal: React.FC<{
);
} else {
signupButton = (
<button className='link-button' onClick={handleSignupClick}>
<button className='link-button' onClick={handleSignupClick} type='button'>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'

View File

@ -22,6 +22,7 @@ import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import type { List } from 'mastodon/models/list';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { messages as membersMessages } from './members';
@ -72,36 +73,23 @@ const MembersLink: React.FC<{
);
};
const NewList: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const NewList: React.FC<{ list?: List | null }> = ({ list }) => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const intl = useIntl();
const history = useHistory();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
const [title, setTitle] = useState('');
const [exclusive, setExclusive] = useState(false);
const [repliesPolicy, setRepliesPolicy] = useState<RepliesPolicyType>('list');
const {
id,
title: initialTitle = '',
exclusive: initialExclusive = false,
replies_policy: initialRepliesPolicy = 'list',
} = list ?? {};
const [title, setTitle] = useState(initialTitle);
const [exclusive, setExclusive] = useState(initialExclusive);
const [repliesPolicy, setRepliesPolicy] =
useState<RepliesPolicyType>(initialRepliesPolicy);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
useEffect(() => {
if (id && list) {
setTitle(list.title);
setExclusive(list.exclusive);
setRepliesPolicy(list.replies_policy);
}
}, [setTitle, setExclusive, setRepliesPolicy, id, list]);
const handleTitleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setTitle(value);
@ -158,6 +146,138 @@ const NewList: React.FC<{
}
}, [history, dispatch, setSubmitting, id, title, exclusive, repliesPolicy]);
return (
<form className='simple_form app-form' onSubmit={handleSubmit}>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_title'>
<FormattedMessage
id='lists.list_name'
defaultMessage='List name'
/>
</label>
<div className='label_input__wrapper'>
<input
id='list_title'
type='text'
value={title}
onChange={handleTitleChange}
maxLength={30}
required
placeholder=' '
/>
</div>
</div>
</div>
</div>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_replies_policy'>
<FormattedMessage
id='lists.show_replies_to'
defaultMessage='Include replies from list members to'
/>
</label>
<div className='label_input__wrapper'>
<select
id='list_replies_policy'
value={repliesPolicy}
onChange={handleRepliesPolicyChange}
>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
>
{(msg) => <option value='none'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.list'
defaultMessage='Members of the list'
>
{(msg) => <option value='list'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
>
{(msg) => <option value='followed'>{msg}</option>}
</FormattedMessage>
</select>
</div>
</div>
</div>
</div>
{id && (
<div className='fields-group'>
<MembersLink id={id} />
</div>
)}
<div className='fields-group'>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong>
<FormattedMessage
id='lists.exclusive'
defaultMessage='Hide members in Home'
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.exclusive_hint'
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={exclusive} onChange={handleExclusiveChange} />
</div>
</div>
</label>
</div>
<div className='actions'>
<button className='button' type='submit'>
{submitting ? (
<LoadingIndicator />
) : id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage id='lists.create' defaultMessage='Create' />
)}
</button>
</div>
</form>
);
};
const NewListWrapper: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { id } = useParams<{ id?: string }>();
const list = useAppSelector((state) =>
id ? state.lists.get(id) : undefined,
);
useEffect(() => {
if (id) {
dispatch(fetchList(id));
}
}, [dispatch, id]);
const isLoading = id && !list;
return (
<Column
bindToDocument={!multiColumn}
@ -172,119 +292,7 @@ const NewList: React.FC<{
/>
<div className='scrollable'>
<form className='simple_form app-form' onSubmit={handleSubmit}>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_title'>
<FormattedMessage
id='lists.list_name'
defaultMessage='List name'
/>
</label>
<div className='label_input__wrapper'>
<input
id='list_title'
type='text'
value={title}
onChange={handleTitleChange}
maxLength={30}
required
placeholder=' '
/>
</div>
</div>
</div>
</div>
<div className='fields-group'>
<div className='input with_label'>
<div className='label_input'>
<label htmlFor='list_replies_policy'>
<FormattedMessage
id='lists.show_replies_to'
defaultMessage='Include replies from list members to'
/>
</label>
<div className='label_input__wrapper'>
<select
id='list_replies_policy'
value={repliesPolicy}
onChange={handleRepliesPolicyChange}
>
<FormattedMessage
id='lists.replies_policy.none'
defaultMessage='No one'
>
{(msg) => <option value='none'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.list'
defaultMessage='Members of the list'
>
{(msg) => <option value='list'>{msg}</option>}
</FormattedMessage>
<FormattedMessage
id='lists.replies_policy.followed'
defaultMessage='Any followed user'
>
{(msg) => <option value='followed'>{msg}</option>}
</FormattedMessage>
</select>
</div>
</div>
</div>
</div>
{id && (
<div className='fields-group'>
<MembersLink id={id} />
</div>
)}
<div className='fields-group'>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
<strong>
<FormattedMessage
id='lists.exclusive'
defaultMessage='Hide members in Home'
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.exclusive_hint'
defaultMessage='If someone is on this list, hide them in your Home feed to avoid seeing their posts twice.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle
checked={exclusive}
onChange={handleExclusiveChange}
/>
</div>
</div>
</label>
</div>
<div className='actions'>
<button className='button' type='submit'>
{submitting ? (
<LoadingIndicator />
) : id ? (
<FormattedMessage id='lists.save' defaultMessage='Save' />
) : (
<FormattedMessage id='lists.create' defaultMessage='Create' />
)}
</button>
</div>
</form>
{isLoading ? <LoadingIndicator /> : <NewList list={list} />}
</div>
<Helmet>
@ -298,4 +306,4 @@ const NewList: React.FC<{
};
// eslint-disable-next-line import/no-default-export
export default NewList;
export default NewListWrapper;

View File

@ -113,7 +113,7 @@ export const MoreLink: React.FC = () => {
return (
<Dropdown items={menu} placement='bottom-start'>
<button className='column-link column-link--transparent'>
<button className='column-link column-link--transparent' type='button'>
<Icon id='' icon={MoreHorizIcon} className='column-link__icon' />
<FormattedMessage id='navigation_bar.more' defaultMessage='More' />

View File

@ -71,6 +71,7 @@ export const SignInBanner: React.FC = () => {
<button
className='button button--block'
onClick={openClosedRegistrationsModal}
type='button'
>
<FormattedMessage
id='sign_in_banner.create_account'

View File

@ -45,6 +45,7 @@ export const FilteredNotificationsIconButton: React.FC<{
title={intl.formatMessage(messages.filteredNotifications)}
onClick={handleClick}
className={className}
type='button'
>
<Icon id='filtered-notifications' icon={InventoryIcon} />
</button>

View File

@ -47,7 +47,7 @@ export const NotificationAnnualReport: React.FC<{
values={{ year }}
/>
</p>
<button onClick={handleClick} className='link-button'>
<button onClick={handleClick} className='link-button' type='button'>
<FormattedMessage
id='notification.annual_report.view'
defaultMessage='View #Wrapstodon'

View File

@ -50,6 +50,7 @@ const BarButton: React.FC<
className={selectedFilter === type ? 'active' : ''}
onClick={onClick}
title={title}
type='button'
>
{children}
</button>

View File

@ -245,6 +245,7 @@ export const Notifications: React.FC<{
title={intl.formatMessage(messages.markAsRead)}
onClick={handleMarkAsRead}
className='column-header__button'
type='button'
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>

View File

@ -9,7 +9,7 @@ export const SearchSection: React.FC<{
<div className='search-results__section__header'>
<h3>{title}</h3>
{onClickMore && (
<button onClick={onClickMore}>
<button onClick={onClickMore} type='button'>
<FormattedMessage
id='search_results.see_all'
defaultMessage='See all'

View File

@ -232,12 +232,14 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectAll}
className={mappedType === 'all' ? 'active' : undefined}
type='button'
>
<FormattedMessage id='search_results.all' defaultMessage='All' />
</button>
<button
onClick={handleSelectAccounts}
className={mappedType === 'accounts' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.accounts'
@ -247,6 +249,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectHashtags}
className={mappedType === 'hashtags' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.hashtags'
@ -256,6 +259,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
<button
onClick={handleSelectStatuses}
className={mappedType === 'statuses' ? 'active' : undefined}
type='button'
>
<FormattedMessage
id='search_results.statuses'

View File

@ -25,7 +25,12 @@ export const ActionsModal: React.FC<{
if (isActionItem(option)) {
element = (
<button onClick={onClick} data-index={i} disabled={disabled}>
<button
onClick={onClick}
data-index={i}
disabled={disabled}
type='button'
>
<DropdownMenuItemContent item={option} />
</button>
);

View File

@ -113,7 +113,7 @@ export const BoostModal: React.FC<{
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<button onClick={handleCancel} className='link-button' type='button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'

View File

@ -58,7 +58,7 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<button onClick={onClose} className='link-button'>
<button onClick={onClose} className='link-button' type='button'>
{cancel ?? (
<FormattedMessage
id='confirmation_modal.cancel'
@ -70,7 +70,11 @@ export const ConfirmationModal: React.FC<
{secondary && (
<>
<div className='spacer' />
<button onClick={handleSecondary} className='link-button'>
<button
onClick={handleSecondary}
className='link-button'
type='button'
>
{secondary}
</button>
</>

View File

@ -196,7 +196,7 @@ export const DomainBlockModal: React.FC<{
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<button onClick={handleCancel} className='link-button' type='button'>
<FormattedMessage
id='confirmation_modal.cancel'
defaultMessage='Cancel'

View File

@ -247,6 +247,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
className='media-modal__nav media-modal__nav--prev'
onClick={handlePrevClick}
aria-label={intl.formatMessage(messages.previous)}
type='button'
>
<Icon id='chevron-left' icon={ChevronLeftIcon} />
</button>
@ -257,6 +258,7 @@ export const MediaModal: FC<MediaModalProps> = forwardRef<
className='media-modal__nav media-modal__nav--next'
onClick={handleNextClick}
aria-label={intl.formatMessage(messages.next)}
type='button'
>
<Icon id='chevron-right' icon={ChevronRightIcon} />
</button>
@ -354,6 +356,7 @@ const MediaPagination: FC<MediaPaginationProps> = ({
active: i === index,
})}
onClick={handleChangeIndex(i)}
type='button'
>
{i + 1}
</button>

View File

@ -127,7 +127,11 @@ const LoginOrSignUp: React.FC = () => {
);
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<button
className='button'
onClick={openClosedRegistrationsModal}
type='button'
>
<FormattedMessage
id='sign_in_banner.create_account'
defaultMessage='Create account'
@ -195,6 +199,7 @@ export const NavigationBar: React.FC = () => {
className={classNames('ui__navigation-bar__item', { active: open })}
onClick={handleClick}
aria-label={intl.formatMessage(messages.menu)}
type='button'
>
<Icon id='' icon={MenuIcon} />
</button>

View File

@ -875,6 +875,7 @@ export const Video: React.FC<{
<button
className='media-gallery__actions__pill'
onClick={toggleReveal}
type='button'
>
<FormattedMessage
id='media_gallery.hide'

View File

@ -249,6 +249,7 @@
"confirmations.missing_alt_text.secondary": "Усё адно апублікаваць",
"confirmations.missing_alt_text.title": "Дадаць альтэрнатыўны тэкст?",
"confirmations.mute.confirm": "Ігнараваць",
"confirmations.private_quote_notify.cancel": "Звяртацца да рэдагавання",
"confirmations.quiet_post_quote_info.dismiss": "Не нагадваць зноў",
"confirmations.quiet_post_quote_info.got_it": "Зразумела",
"confirmations.quiet_post_quote_info.message": "Калі будзеце цытаваць ціхі публічны допіс, Ваш допіс будзе схаваны ад трэндавых стужак.",

View File

@ -192,6 +192,7 @@
"community.column_settings.local_only": "Només local",
"community.column_settings.media_only": "Només contingut",
"community.column_settings.remote_only": "Només remot",
"compose.error.blank_post": "La publicació no pot estar en blanc.",
"compose.language.change": "Canvia d'idioma",
"compose.language.search": "Cerca idiomes...",
"compose.published.body": "Tut publicat.",
@ -398,6 +399,7 @@
"follow_suggestions.who_to_follow": "A qui seguir",
"followed_tags": "Etiquetes seguides",
"footer.about": "Quant a",
"footer.about_this_server": "Quant a",
"footer.directory": "Directori de perfils",
"footer.get_app": "Aconsegueix l'app",
"footer.keyboard_shortcuts": "Dreceres de teclat",
@ -745,7 +747,9 @@
"privacy.unlisted.short": "Públic silenciós",
"privacy_policy.last_updated": "Darrera actualització {date}",
"privacy_policy.title": "Política de Privacitat",
"quote_error.edit": "No es poden afegir cites en editar una publicació.",
"quote_error.poll": "Amb les enquestes no es permeten cites.",
"quote_error.private_mentions": "Amb mencions directes no es permeten cites.",
"quote_error.quote": "Només es permet una cita alhora.",
"quote_error.unauthorized": "No se us permet de citar aquesta publicació.",
"quote_error.upload": "Amb media adjunts no es permeten cites.",
@ -867,6 +871,7 @@
"status.contains_quote": "Conté una cita",
"status.context.loading": "Es carreguen més respostes",
"status.context.loading_error": "No s'han pogut carregar respostes noves",
"status.context.loading_success": "S'han carregat les noves respostes",
"status.context.more_replies_found": "S'han trobat més respostes",
"status.context.retry": "Torna-ho a provar",
"status.context.show": "Mostra",
@ -898,9 +903,12 @@
"status.pin": "Fixa en el perfil",
"status.quote": "Cita",
"status.quote.cancel": "Canceŀlar la citació",
"status.quote_error.blocked_account_hint.title": "Aquesta publicació està amagada perquè heu blocat a @{name}.",
"status.quote_error.blocked_domain_hint.title": "Aquesta publicació està amagada perquè heu blocat a {domain}.",
"status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres",
"status.quote_error.limited_account_hint.action": "Mostra-la igualment",
"status.quote_error.limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.",
"status.quote_error.muted_account_hint.title": "Aquesta publicació està amagada perquè heu silenciat a @{name}.",
"status.quote_error.not_available": "Publicació no disponible",
"status.quote_error.pending_approval": "Publicació pendent",
"status.quote_error.pending_approval_popout.body": "A Mastodon pots controlar si algú et pot citar. Aquesta publicació està pendent mentre esperem l'aprovació de l'autor original.",
@ -999,6 +1007,7 @@
"visibility_modal.helper.direct_quoting": "No es poden citar mencions privades fetes a Mastondon.",
"visibility_modal.helper.private_quoting": "No es poden citar publicacions fetes a Mastodon només per a seguidors.",
"visibility_modal.helper.unlisted_quoting": "Quan la gent et citi les seves publicacions estaran amagades de les línies de temps de tendències.",
"visibility_modal.instructions": "Controleu qui pot interactuar amb aquesta publicació. També podeu aplicar la configuració a totes les publicacions futures navegant a <link>Preferències > Valors per defecte de publicació</link>.",
"visibility_modal.privacy_label": "Visibilitat",
"visibility_modal.quote_followers": "Només seguidors",
"visibility_modal.quote_label": "Qui pot citar",

View File

@ -196,6 +196,7 @@
"community.column_settings.local_only": "Pouze místní",
"community.column_settings.media_only": "Pouze média",
"community.column_settings.remote_only": "Pouze vzdálené",
"compose.error.blank_post": "Příspěvek nemůže být prázdný.",
"compose.language.change": "Změnit jazyk",
"compose.language.search": "Prohledat jazyky...",
"compose.published.body": "Příspěvek zveřejněn.",
@ -248,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Přesto odeslat",
"confirmations.missing_alt_text.title": "Přidat popisek?",
"confirmations.mute.confirm": "Skrýt",
"confirmations.private_quote_notify.cancel": "Zpět k úpravám",
"confirmations.private_quote_notify.confirm": "Publikovat příspěvek",
"confirmations.private_quote_notify.do_not_show_again": "Nezobrazujte mi znovu tuto zprávu",
"confirmations.private_quote_notify.message": "Osoba, kterou citujete, a další zmínění budou upozorněni a budou moci si zobrazit váš příspěvek, i pokud vás nesledují.",
"confirmations.private_quote_notify.title": "Sdílet se sledujícími a zmíněnými uživateli?",
"confirmations.quiet_post_quote_info.dismiss": "Znovu nepřípomínat",
"confirmations.quiet_post_quote_info.got_it": "Rozumím",
"confirmations.quiet_post_quote_info.message": "Při citování ztišeného veřejného příspěvku, váš příspěvek bude skrytý z os populárních příspěvků.",
@ -407,6 +413,7 @@
"follow_suggestions.who_to_follow": "Koho sledovat",
"followed_tags": "Sledované hashtagy",
"footer.about": "O aplikaci",
"footer.about_this_server": "O serveru",
"footer.directory": "Adresář profilů",
"footer.get_app": "Získat aplikaci",
"footer.keyboard_shortcuts": "Klávesové zkratky",
@ -758,6 +765,7 @@
"privacy_policy.title": "Zásady ochrany osobních údajů",
"quote_error.edit": "Citáty nemohou být přidány při úpravě příspěvku.",
"quote_error.poll": "Citování není u dotazníků povoleno.",
"quote_error.private_mentions": "Citování není povoleno s přímými zmínkami.",
"quote_error.quote": "Je povoleno citovat pouze jednou.",
"quote_error.unauthorized": "Nemáte oprávnění citovat tento příspěvek.",
"quote_error.upload": "Není povoleno citovat s přílohami.",
@ -1011,6 +1019,8 @@
"video.volume_down": "Snížit hlasitost",
"video.volume_up": "Zvýšit hlasitost",
"visibility_modal.button_title": "Nastavit viditelnost",
"visibility_modal.direct_quote_warning.text": "Pokud uložíte aktuální nastavení, vložená citace bude převedena na odkaz.",
"visibility_modal.direct_quote_warning.title": "Citace nemohou být vloženy do soukromých zmínek",
"visibility_modal.header": "Viditelnost a interakce",
"visibility_modal.helper.direct_quoting": "Soukromé zmínky, které jsou vytvořeny na Mastodonu, nemohou být citovány ostatními.",
"visibility_modal.helper.privacy_editing": "Viditelnost nelze změnit po publikování příspěvku.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Læg op alligevel",
"confirmations.missing_alt_text.title": "Tilføj alt-tekst?",
"confirmations.mute.confirm": "Skjul",
"confirmations.private_quote_notify.cancel": "Tilbage til redigering",
"confirmations.private_quote_notify.confirm": "Offentliggør indlæg",
"confirmations.private_quote_notify.do_not_show_again": "Vis ikke denne besked igen",
"confirmations.private_quote_notify.message": "Den person, du citerer og andre omtalte vil blive underrettet, og vil være i stand til at se dit indlæg, selv om de ikke følger dig.",
"confirmations.private_quote_notify.title": "Del med følgere og omtalte brugere?",
"confirmations.quiet_post_quote_info.dismiss": "Påmind mig ikke igen",
"confirmations.quiet_post_quote_info.got_it": "Forstået",
"confirmations.quiet_post_quote_info.message": "Når du citerer et stille offentligt indlæg, vil dit indlæg blive skjult fra trendtidslinjer.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Privatlivspolitik",
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
"quote_error.poll": "Citering ikke tilladt i afstemninger.",
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
"quote_error.quote": "Kun ét citat ad gangen er tilladt.",
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
"quote_error.upload": "Citering ikke tilladt ved medievedhæftninger.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Lydstyrke ned",
"video.volume_up": "Lydstyrke op",
"visibility_modal.button_title": "Indstil synlighed",
"visibility_modal.direct_quote_warning.text": "Hvis du gemmer de aktuelle indstillinger, vil det indlejrede citat blive konverteret til et link.",
"visibility_modal.direct_quote_warning.title": "Citater kan ikke indlejres i private omtaler",
"visibility_modal.header": "Synlighed og interaktion",
"visibility_modal.helper.direct_quoting": "Private omtaler forfattet på Mastodon kan ikke citeres af andre.",
"visibility_modal.helper.privacy_editing": "Synlighed kan ikke ændres, efter at et indlæg er offentliggjort.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Trotzdem veröffentlichen",
"confirmations.missing_alt_text.title": "Bildbeschreibung hinzufügen?",
"confirmations.mute.confirm": "Stummschalten",
"confirmations.private_quote_notify.cancel": "Zurück zum Bearbeiten",
"confirmations.private_quote_notify.confirm": "Beitrag veröffentlichen",
"confirmations.private_quote_notify.do_not_show_again": "Diese Nachricht nicht erneut anzeigen",
"confirmations.private_quote_notify.message": "Die zitierte Person und andere erwähnte Personen werden benachrichtigt und können Ihren Beitrag sehen, auch wenn sie dir nicht folgen.",
"confirmations.private_quote_notify.title": "Mit Followern und erwähnten Nutzern teilen?",
"confirmations.quiet_post_quote_info.dismiss": "Nicht mehr anzeigen",
"confirmations.quiet_post_quote_info.got_it": "Verstanden",
"confirmations.quiet_post_quote_info.message": "Beim Zitieren eines Beitrags mit der Sichtbarkeit „Öffentlich (still)“ wird dein zitierter Beitrag ebenfalls nicht in den Trends und öffentlichen Timelines angezeigt.",
@ -1013,6 +1018,8 @@
"video.volume_down": "Leiser",
"video.volume_up": "Lauter",
"visibility_modal.button_title": "Sichtbarkeit festlegen",
"visibility_modal.direct_quote_warning.text": "Wenn die aktuellen Einstellungen gespeichert werden, wird das eingebettete Zitat in einen Link umgewandelt.",
"visibility_modal.direct_quote_warning.title": "Zitate können nicht in private Erwähnungen eingebettet werden",
"visibility_modal.header": "Sichtbarkeit und Interaktion",
"visibility_modal.helper.direct_quoting": "Private Erwähnungen, die auf Mastodon verfasst wurden, können nicht von anderen zitiert werden.",
"visibility_modal.helper.privacy_editing": "Die Sichtbarkeit eines bereits veröffentlichten Beitrags kann nachträglich nicht mehr geändert werden.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει",
"confirmations.missing_alt_text.title": "Προσθήκη εναλλακτικού κειμένου;",
"confirmations.mute.confirm": "Αποσιώπηση",
"confirmations.private_quote_notify.cancel": "Πίσω στην επεξεργασία",
"confirmations.private_quote_notify.confirm": "Δημοσίευση ανάρτησης",
"confirmations.private_quote_notify.do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα",
"confirmations.private_quote_notify.message": "Το άτομο που παραθέτετε και άλλες επισημάνσεις θα ειδοποιηθούν και θα μπορούν να δουν την ανάρτησή σας, ακόμη και αν δεν σας ακολουθούν.",
"confirmations.private_quote_notify.title": "Κοινοποίηση με τους ακολούθους και τους επισημασμένους χρήστες;",
"confirmations.quiet_post_quote_info.dismiss": "Μη μου το ξαναθυμίσεις",
"confirmations.quiet_post_quote_info.got_it": "Το κατάλαβα",
"confirmations.quiet_post_quote_info.message": "Όταν παραθέτετε μια ήσυχη δημόσια ανάρτηση, η ανάρτηση σας θα είναι κρυμμένη από τις δημοφιλείς ροές.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Πολιτική Απορρήτου",
"quote_error.edit": "Δεν μπορούν να προστεθούν παραθέσεις κατά την επεξεργασία μιας ανάρτησης.",
"quote_error.poll": "Η παράθεση δεν επιτρέπεται με δημοσκοπήσεις.",
"quote_error.private_mentions": "Η παράθεση δεν επιτρέπεται με άμεσες επισημάνσεις.",
"quote_error.quote": "Επιτρέπεται μόνο μία παράθεση τη φορά.",
"quote_error.unauthorized": "Δεν είστε εξουσιοδοτημένοι να παραθέσετε αυτή την ανάρτηση.",
"quote_error.upload": "Η παράθεση δεν επιτρέπεται με συνημμένα πολυμέσων.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Μείωση έντασης",
"video.volume_up": "Αύξηση έντασης",
"visibility_modal.button_title": "Ορισμός ορατότητας",
"visibility_modal.direct_quote_warning.text": "Εάν αποθηκεύσετε τις τρέχουσες ρυθμίσεις, η ενσωματωμένη παράθεση θα μετατραπεί σε σύνδεσμο.",
"visibility_modal.direct_quote_warning.title": "Οι παραθέσεις δεν μπορούν να ενσωματωθούν σε ιδιωτικές επισημάνσεις",
"visibility_modal.header": "Ορατότητα και αλληλεπίδραση",
"visibility_modal.helper.direct_quoting": "Ιδιωτικές αναφορές που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.",
"visibility_modal.helper.privacy_editing": "Η ορατότητα δεν μπορεί να αλλάξει μετά τη δημοσίευση μιας ανάρτησης.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Enviar de todos modos",
"confirmations.missing_alt_text.title": "¿Agregar texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.private_quote_notify.cancel": "Volver a editar",
"confirmations.private_quote_notify.confirm": "Enviar mensaje",
"confirmations.private_quote_notify.do_not_show_again": "No mostrarme este mensaje de nuevo",
"confirmations.private_quote_notify.message": "La cuenta a la que estás citando y otras menciones serán notificadas y podrán ver tu mensaje, incluso si no te están siguiendo.",
"confirmations.private_quote_notify.title": "¿Compartir con seguidores y usuarios mencionados?",
"confirmations.quiet_post_quote_info.dismiss": "No recordar de nuevo",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Al citar un mensaje público pero silencioso, tu mensaje se ocultará de las líneas temporales de tendencias.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Política de privacidad",
"quote_error.edit": "Las citas no se pueden agregar al editar un mensaje.",
"quote_error.poll": "No se permite citar encuestas.",
"quote_error.private_mentions": "No se permite citar con menciones directas.",
"quote_error.quote": "Solo se permite una cita a la vez.",
"quote_error.unauthorized": "No tenés autorización para citar este mensaje.",
"quote_error.upload": "No se permite citar con archivos multimedia.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Bajar volumen",
"video.volume_up": "Subir volumen",
"visibility_modal.button_title": "Establecer visibilidad",
"visibility_modal.direct_quote_warning.text": "Si guardás la configuración actual, la cita insertada se convertirá en un enlace.",
"visibility_modal.direct_quote_warning.title": "Las citas no pueden ser insertadas en menciones privadas",
"visibility_modal.header": "Visibilidad e interacción",
"visibility_modal.helper.direct_quoting": "Las menciones privadas redactadas en Mastodon no pueden ser citadas por otras cuentas.",
"visibility_modal.helper.privacy_editing": "La visibilidad no se puede cambiar después de que se haya enviado un mensaje.",

View File

@ -157,6 +157,7 @@
"bundle_modal_error.close": "Pechar",
"bundle_modal_error.message": "Algo fallou mentras cargaba esta páxina.",
"bundle_modal_error.retry": "Téntao de novo",
"carousel.current": "<sr>Filmina</sr> {current, number} / {max, number}",
"carousel.slide": "Filmina {current, number} de {max, number}",
"closed_registrations.other_server_instructions": "Cómo Mastodon é descentralizado, podes crear unha conta noutro servidor e interactuar igualmente con este.",
"closed_registrations_modal.description": "Actualmente non é posible crear unha conta en {domain}, pero ten en conta que non precisas unha conta específicamente en {domain} para usar Mastodon.",
@ -248,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Publicar igualmente",
"confirmations.missing_alt_text.title": "Engadir texto descritivo?",
"confirmations.mute.confirm": "Acalar",
"confirmations.private_quote_notify.cancel": "Volver á edición",
"confirmations.private_quote_notify.confirm": "Publicar publicación",
"confirmations.private_quote_notify.do_not_show_again": "Non me volvas a mostrar esta mensaxe",
"confirmations.private_quote_notify.message": "A persoa que estás a citar e outras contas mencionadas serán notificadas e poderán ver a túa publicación, incluso se non te seguen.",
"confirmations.private_quote_notify.title": "Compartir coas seguidoras e coas usuarias mencionadas?",
"confirmations.quiet_post_quote_info.dismiss": "Non lembrarmo máis",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Ao citar unha publicación pública limitada a túa publicación non aparecerá nas cronoloxías de tendencias.",
@ -759,6 +765,7 @@
"privacy_policy.title": "Política de Privacidade",
"quote_error.edit": "Non se poden engadir citas ao editar unha publicación.",
"quote_error.poll": "Non se permite citar as enquisas.",
"quote_error.private_mentions": "As citas non están permitidas nas mencións directas.",
"quote_error.quote": "Só se permite citar unha vez.",
"quote_error.unauthorized": "Non tes permiso para citar esta publicación.",
"quote_error.upload": "As citas non están permitidas para anexos multimedia.",
@ -1012,6 +1019,8 @@
"video.volume_down": "Baixar volume",
"video.volume_up": "Subir volume",
"visibility_modal.button_title": "Establece a visibilidade",
"visibility_modal.direct_quote_warning.text": "Se gardas os axustes actuais a cita incluída vaise converter nunha ligazón.",
"visibility_modal.direct_quote_warning.title": "As citas non se poden incluír nas mencións privadas",
"visibility_modal.header": "Visibilidade e interaccións",
"visibility_modal.helper.direct_quoting": "As mencións privadas creadas con Mastodon non poden ser citadas.",
"visibility_modal.helper.privacy_editing": "Non se pode cambiar a visibilidade unha vez foi publicada.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "לפרסם בכל זאת",
"confirmations.missing_alt_text.title": "להוסיף מלל חלופי?",
"confirmations.mute.confirm": "להשתיק",
"confirmations.private_quote_notify.cancel": "חזרה לעריכה",
"confirmations.private_quote_notify.confirm": "פרסום ההודעה",
"confirmations.private_quote_notify.do_not_show_again": "אל תראה לי הודעה זו שוב",
"confirmations.private_quote_notify.message": "המצוטט ומאוזכרים אחרים יקבלו התראה ויוכלו לקרוא את ההודעה שלך, אפילו אם אינם עוקבים אחריך.",
"confirmations.private_quote_notify.title": "לשתף עם עוקבים ומאוזכרים?",
"confirmations.quiet_post_quote_info.dismiss": "אל תזכיר לי שוב",
"confirmations.quiet_post_quote_info.got_it": "הבנתי",
"confirmations.quiet_post_quote_info.message": "כשמצטטים הודעה ציבורית שקטה, גם ההודעה המצטטת תוסתר מחלון הנושאים החמים.",
@ -760,6 +765,7 @@
"privacy_policy.title": "מדיניות פרטיות",
"quote_error.edit": "לא ניתן להוסיף ציטוטים בשלב עריכת ההודעה.",
"quote_error.poll": "לא ניתן לכלול משאל כאשר מחברים הודעת ציטוט.",
"quote_error.private_mentions": "ציטוט אינו אפשרי בהודעות פרטיות.",
"quote_error.quote": "רק ציטוט אחד מותר בכל הודעה.",
"quote_error.unauthorized": "אין לך הרשאה לצטט את ההודעה הזו.",
"quote_error.upload": "ציטוט הכולל מדיה אינו אפשרי.",
@ -1013,6 +1019,8 @@
"video.volume_down": "הנמכת עוצמת השמע",
"video.volume_up": "הגברת עוצמת שמע",
"visibility_modal.button_title": "בחירת רמת חשיפה",
"visibility_modal.direct_quote_warning.text": "אם תשמרו את ההעדפות הנוכחיות, הציטוט המוטמע יהפוך לקישור.",
"visibility_modal.direct_quote_warning.title": "לא ניתן להטמיע ציטוטים בהודעות פרטיות",
"visibility_modal.header": "חשיפה והידוּד (אינטראקציה)",
"visibility_modal.helper.direct_quoting": "איזכורים פרטיים שנוצרו במסטודון חסומים מציטוט על ידי אחרים.",
"visibility_modal.helper.privacy_editing": "רמת החשיפה של ההודעה לא ניתנת לשינוי אחרי הפרסום.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Közzététel mindenképpen",
"confirmations.missing_alt_text.title": "Helyettesítő szöveg hozzáadása?",
"confirmations.mute.confirm": "Némítás",
"confirmations.private_quote_notify.cancel": "Vissza a szerkesztéshez",
"confirmations.private_quote_notify.confirm": "Bejegyzés közzététele",
"confirmations.private_quote_notify.do_not_show_again": "Ne jelenítse meg újra ezt az üzenetet",
"confirmations.private_quote_notify.message": "Akit idézel, és a többi megemlített értesítve lesz, és megtekinthetik a bejegyzésed, még akkor is, ha nem követnek.",
"confirmations.private_quote_notify.title": "Megosztás a követőkkel és a megemlített felhasználókkal?",
"confirmations.quiet_post_quote_info.dismiss": "Ne emlékeztessen újra",
"confirmations.quiet_post_quote_info.got_it": "Rendben",
"confirmations.quiet_post_quote_info.message": "Ha csendes nyilvános bejegyzést idézel, akkor a bejegyzés el lesz rejtve a felkapottak idővonalairól.",
@ -1013,6 +1018,8 @@
"video.volume_down": "Hangerő le",
"video.volume_up": "Hangerő fel",
"visibility_modal.button_title": "Láthatóság beállítása",
"visibility_modal.direct_quote_warning.text": "Ha mented a jelenlegi beállításokat, akkor a beágyazott idézet hivatkozássá lesz alakítva.",
"visibility_modal.direct_quote_warning.title": "Idézetek nem ágyazhatóak be privát említésekbe",
"visibility_modal.header": "Láthatóság és interakció",
"visibility_modal.helper.direct_quoting": "A Mastodonon készült privát említéseket mások nem idézhetik.",
"visibility_modal.helper.privacy_editing": "A láthatóság nem módosítható a bejegyzés közzététele után.",

View File

@ -249,6 +249,11 @@
"confirmations.missing_alt_text.secondary": "Birta samt",
"confirmations.missing_alt_text.title": "Bæta við hjálpartexta?",
"confirmations.mute.confirm": "Þagga",
"confirmations.private_quote_notify.cancel": "Til baka í breytingar",
"confirmations.private_quote_notify.confirm": "Birta færslu",
"confirmations.private_quote_notify.do_not_show_again": "Ekki birta þessi skilaboð aftur",
"confirmations.private_quote_notify.message": "Sá sem vitnað er í og aðrir sem minnst er á verða látin vita og munu geta skoðað færsluna þína, jafnvel þótt viðkomandi fylgist ekki með þér.",
"confirmations.private_quote_notify.title": "Deila með fylgjendum og þeim sem minnst er á?",
"confirmations.quiet_post_quote_info.dismiss": "Ekki minna mig aftur á þetta",
"confirmations.quiet_post_quote_info.got_it": "Náði því",
"confirmations.quiet_post_quote_info.message": "Þegar þú vitnar í hljóðláta opinbera færslu, verður færslan þín ekki birt á vinsældatímalínum.",
@ -760,6 +765,7 @@
"privacy_policy.title": "Persónuverndarstefna",
"quote_error.edit": "Ekki er hægt að bæta við tilvitnunum þegar færslum er breytt.",
"quote_error.poll": "Ekki er leyft að vitna í kannanir.",
"quote_error.private_mentions": "Tilvitnanir eru ekki leyfðar í beinu einkaspjalli.",
"quote_error.quote": "Einungis ein tilvitnun er leyfð í einu.",
"quote_error.unauthorized": "Þú hefur ekki heimild til að vitna í þessa færslu.",
"quote_error.upload": "Ekki er leyft að vitna í myndviðhengi.",
@ -1013,6 +1019,8 @@
"video.volume_down": "Lækka hljóðstyrk",
"video.volume_up": "Hækka hljóðstyrk",
"visibility_modal.button_title": "Stilla sýnileika",
"visibility_modal.direct_quote_warning.text": "Ef þú vistar þessa stillingu, þá verður ívöfnu tilvitnuninni breytt í tengil.",
"visibility_modal.direct_quote_warning.title": "Ekki er hægt að ívefja tilvitnanir í beinu einkaspjalli",
"visibility_modal.header": "Sýnileiki og gagnvirkni",
"visibility_modal.helper.direct_quoting": "Ekki er hægt að vitna í einkaspjall sem skrifað er á Mastodon.",
"visibility_modal.helper.privacy_editing": "Ekki er hægt að breyta sýnileika færslu eftir að hún hefur verið birt.",

Some files were not shown because too many files have changed in this diff Show More