Merge pull request #3268 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 58b3fc03796dbde54dbba9a4c554c2d8d24f81fd
This commit is contained in:
commit
1a890d2077
2
.github/renovate.json5
vendored
2
.github/renovate.json5
vendored
@ -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',
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -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)
|
||||
|
||||
2
Gemfile
2
Gemfile
@ -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'
|
||||
|
||||
17
Gemfile.lock
17
Gemfile.lock
@ -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)
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -237,6 +237,7 @@ export const DropdownMenu = <Item = MenuItem,>({
|
||||
onKeyUp={handleItemKeyUp}
|
||||
data-index={i}
|
||||
aria-disabled={disabled}
|
||||
type='button'
|
||||
>
|
||||
<DropdownMenuItemContent item={option} />
|
||||
</button>
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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}}'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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}}'
|
||||
|
||||
@ -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>{' '}
|
||||
·{' '}
|
||||
|
||||
@ -232,6 +232,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
|
||||
ref={focusRefCallback}
|
||||
aria-disabled={disabled}
|
||||
data-index={index}
|
||||
type='button'
|
||||
>
|
||||
<DropdownMenuItemContent item={item} />
|
||||
</button>
|
||||
|
||||
@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
|
||||
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type='button'
|
||||
className='link-button'
|
||||
onClick={onClick}
|
||||
aria-describedby={descriptionId}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
100
app/javascript/flavours/glitch/features/audio/visualizer.tsx
Normal file
100
app/javascript/flavours/glitch/features/audio/visualizer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -51,6 +51,7 @@ const BarButton: React.FC<
|
||||
className={selectedFilter === type ? 'active' : ''}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
type='button'
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -881,6 +881,7 @@ export const Video: React.FC<{
|
||||
<button
|
||||
className='media-gallery__actions__pill'
|
||||
onClick={toggleReveal}
|
||||
type='button'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='media_gallery.hide'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -237,6 +237,7 @@ export const DropdownMenu = <Item = MenuItem,>({
|
||||
onKeyUp={handleItemKeyUp}
|
||||
data-index={i}
|
||||
aria-disabled={disabled}
|
||||
type='button'
|
||||
>
|
||||
<DropdownMenuItemContent item={option} />
|
||||
</button>
|
||||
|
||||
@ -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}'
|
||||
|
||||
@ -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}}'
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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}}'
|
||||
|
||||
@ -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>{' '}
|
||||
·{' '}
|
||||
|
||||
@ -232,6 +232,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
|
||||
ref={focusRefCallback}
|
||||
aria-disabled={disabled}
|
||||
data-index={index}
|
||||
type='button'
|
||||
>
|
||||
<DropdownMenuItemContent item={item} />
|
||||
</button>
|
||||
|
||||
@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
|
||||
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type='button'
|
||||
className='link-button'
|
||||
onClick={onClick}
|
||||
aria-describedby={descriptionId}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
100
app/javascript/mastodon/features/audio/visualizer.tsx
Normal file
100
app/javascript/mastodon/features/audio/visualizer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -50,6 +50,7 @@ const BarButton: React.FC<
|
||||
className={selectedFilter === type ? 'active' : ''}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
type='button'
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -875,6 +875,7 @@ export const Video: React.FC<{
|
||||
<button
|
||||
className='media-gallery__actions__pill'
|
||||
onClick={toggleReveal}
|
||||
type='button'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='media_gallery.hide'
|
||||
|
||||
@ -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": "Калі будзеце цытаваць ціхі публічны допіс, Ваш допіс будзе схаваны ад трэндавых стужак.",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "Η ορατότητα δεν μπορεί να αλλάξει μετά τη δημοσίευση μιας ανάρτησης.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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": "רמת החשיפה של ההודעה לא ניתנת לשינוי אחרי הפרסום.",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user