Merge commit '58b3fc03796dbde54dbba9a4c554c2d8d24f81fd' into glitch-soc/merge-upstream
This commit is contained in:
commit
58b29be439
143 files changed
+821
-469
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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'>
|
||||
|
||||
+18
-6
@@ -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>
|
||||
|
||||
+3
-1
@@ -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'
|
||||
|
||||
+1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
+6
-2
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user