Merge remote-tracking branch 'parent/main' into upstream-20241126

This commit is contained in:
KMY 2024-11-26 12:56:31 +09:00
commit 8a075ba4c6
303 changed files with 7495 additions and 4498 deletions

View file

@ -1,82 +0,0 @@
// Kmyblue tracking marker: copied antennas/new_antenna_form, circles/new_circle_form, bookmark_categories/new_bookmark_category_form
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { changeListEditorTitle, submitListEditor } from 'mastodon/actions/lists';
import { Button } from 'mastodon/components/button';
const messages = defineMessages({
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
});
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'title']),
disabled: state.getIn(['listEditor', 'isSubmitting']),
});
const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeListEditorTitle(value)),
onSubmit: () => dispatch(submitListEditor(true)),
});
class NewListForm extends PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};
handleChange = e => {
this.props.onChange(e.target.value);
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
};
handleClick = () => {
this.props.onSubmit();
};
render () {
const { value, disabled, intl } = this.props;
const label = intl.formatMessage(messages.label);
const title = intl.formatMessage(messages.title);
return (
<form className='column-inline-form' onSubmit={this.handleSubmit}>
<label>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text'
value={value}
disabled={disabled}
onChange={this.handleChange}
placeholder={label}
/>
</label>
<Button
disabled={disabled || !value}
text={title}
onClick={this.handleClick}
/>
</form>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewListForm));

View file

@ -1,98 +0,0 @@
// Kmyblue tracking marker: copied antennas, circles, bookmark_categories
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import ColumnLink from 'mastodon/features/ui/components/column_link';
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
import NewListForm from './components/new_list_form';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
with_antenna: { id: 'lists.with_antenna', defaultMessage: 'Antenna' },
});
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
lists: getOrderedLists(state),
});
class Lists extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
lists: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
this.props.dispatch(fetchLists());
}
render () {
const { intl, lists, multiColumn } = this.props;
if (!lists) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='list-ul' iconComponent={ListAltIcon} multiColumn={multiColumn} />
<NewListForm />
<ScrollableList
scrollKey='lists'
emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
bindToDocument={!multiColumn}
>
{lists.map(list =>
(<ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' iconComponent={ListAltIcon} text={list.get('title')}
badge={(list.get('antennas') && list.get('antennas').size > 0) ? intl.formatMessage(messages.with_antenna) : undefined} />),
)}
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(injectIntl(Lists));

View file

@ -0,0 +1,145 @@
import { useEffect, useMemo, useCallback } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link } from 'react-router-dom';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchLists } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import Column from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { getOrderedLists } from 'mastodon/selectors/lists';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
create: { id: 'lists.create_list', defaultMessage: 'Create list' },
edit: { id: 'lists.edit', defaultMessage: 'Edit list' },
delete: { id: 'lists.delete', defaultMessage: 'Delete list' },
more: { id: 'status.more', defaultMessage: 'More' },
});
const ListItem: React.FC<{
id: string;
title: string;
}> = ({ id, title }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const handleDeleteClick = useCallback(() => {
dispatch(
openModal({
modalType: 'CONFIRM_DELETE_LIST',
modalProps: {
listId: id,
},
}),
);
}, [dispatch, id]);
const menu = useMemo(
() => [
{ text: intl.formatMessage(messages.edit), to: `/lists/${id}/edit` },
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
],
[intl, id, handleDeleteClick],
);
return (
<div className='lists__item'>
<Link to={`/lists/${id}`} className='lists__item__title'>
<Icon id='list-ul' icon={ListAltIcon} />
<span>{title}</span>
</Link>
<DropdownMenuContainer
scrollKey='lists'
items={menu}
icons='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
</div>
);
};
const Lists: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const lists = useAppSelector((state) => getOrderedLists(state));
useEffect(() => {
dispatch(fetchLists());
}, [dispatch]);
const emptyMessage = (
<>
<span>
<FormattedMessage
id='lists.no_lists_yet'
defaultMessage='No lists yet.'
/>
<br />
<FormattedMessage
id='lists.create_a_list_to_organize'
defaultMessage='Create a new list to organize your Home feed'
/>
</span>
<SquigglyArrow className='empty-column-indicator__arrow' />
</>
);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.heading)}
>
<ColumnHeader
title={intl.formatMessage(messages.heading)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
extraButton={
<Link
to='/lists/new'
className='column-header__button'
title={intl.formatMessage(messages.create)}
aria-label={intl.formatMessage(messages.create)}
>
<Icon id='plus' icon={AddIcon} />
</Link>
}
/>
<ScrollableList
scrollKey='lists'
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{lists.map((list) => (
<ListItem key={list.id} id={list.id} title={list.title} />
))}
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default Lists;

View file

@ -0,0 +1,373 @@
import { useCallback, useState, useEffect, useRef } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams, Link } from 'react-router-dom';
import { useDebouncedCallback } from 'use-debounce';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
import { fetchFollowing } from 'mastodon/actions/accounts';
import { importFetchedAccounts } from 'mastodon/actions/importer';
import { fetchList } from 'mastodon/actions/lists';
import { apiRequest } from 'mastodon/api';
import {
apiGetAccounts,
apiAddAccountToList,
apiRemoveAccountFromList,
} from 'mastodon/api/lists';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import Column from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { FollowersCounter } from 'mastodon/components/counters';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
placeholder: {
id: 'lists.search_placeholder',
defaultMessage: 'Search people you follow',
},
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
add: { id: 'lists.add_member', defaultMessage: 'Add' },
remove: { id: 'lists.remove_member', defaultMessage: 'Remove' },
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
});
type Mode = 'remove' | 'add';
const ColumnSearchHeader: React.FC<{
onBack: () => void;
onSubmit: (value: string) => void;
}> = ({ onBack, onSubmit }) => {
const intl = useIntl();
const [value, setValue] = useState('');
const handleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setValue(value);
onSubmit(value);
},
[setValue, onSubmit],
);
const handleSubmit = useCallback(() => {
onSubmit(value);
}, [onSubmit, value]);
return (
<ButtonInTabsBar>
<form className='column-search-header' onSubmit={handleSubmit}>
<button
type='button'
className='column-header__back-button compact'
onClick={onBack}
aria-label={intl.formatMessage(messages.back)}
>
<Icon
id='chevron-left'
icon={ArrowBackIcon}
className='column-back-button__icon'
/>
</button>
<input
type='search'
value={value}
onChange={handleChange}
placeholder={intl.formatMessage(messages.placeholder)}
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus
/>
</form>
</ButtonInTabsBar>
);
};
const AccountItem: React.FC<{
accountId: string;
listId: string;
partOfList: boolean;
onToggle: (accountId: string) => void;
}> = ({ accountId, listId, partOfList, onToggle }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(accountId));
const handleClick = useCallback(() => {
if (partOfList) {
void apiRemoveAccountFromList(listId, accountId);
} else {
void apiAddAccountToList(listId, accountId);
}
onToggle(accountId);
}, [accountId, listId, partOfList, onToggle]);
if (!account) {
return null;
}
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
return (
<div className='account'>
<div className='account__wrapper'>
<Link
key={account.id}
className='account__display-name'
title={account.acct}
to={`/@${account.acct}`}
data-hover-card-account={account.id}
>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={36} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
<div className='account__details'>
<ShortNumber
value={account.followers_count}
renderer={FollowersCounter}
/>{' '}
{firstVerifiedField && (
<VerifiedBadge link={firstVerifiedField.value} />
)}
</div>
</div>
</Link>
<div className='account__relationship'>
<Button
text={intl.formatMessage(
partOfList ? messages.remove : messages.add,
)}
onClick={handleClick}
/>
</div>
</div>
</div>
);
};
const ListMembers: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id: string }>();
const intl = useIntl();
const followingAccountIds = useAppSelector(
(state) => state.user_lists.getIn(['following', me, 'items']) as string[],
);
const [searching, setSearching] = useState(false);
const [accountIds, setAccountIds] = useState<string[]>([]);
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [mode, setMode] = useState<Mode>('remove');
useEffect(() => {
if (id) {
setLoading(true);
dispatch(fetchList(id));
void apiGetAccounts(id)
.then((data) => {
dispatch(importFetchedAccounts(data));
setAccountIds(data.map((a) => a.id));
setLoading(false);
return '';
})
.catch(() => {
setLoading(false);
});
dispatch(fetchFollowing(me));
}
}, [dispatch, id]);
const handleSearchClick = useCallback(() => {
setMode('add');
}, [setMode]);
const handleDismissSearchClick = useCallback(() => {
setMode('remove');
setSearching(false);
}, [setMode]);
const handleAccountToggle = useCallback(
(accountId: string) => {
const partOfList = accountIds.includes(accountId);
if (partOfList) {
setAccountIds(accountIds.filter((id) => id !== accountId));
} else {
setAccountIds([accountId, ...accountIds]);
}
},
[accountIds, setAccountIds],
);
const searchRequestRef = useRef<AbortController | null>(null);
const handleSearch = useDebouncedCallback(
(value: string) => {
if (searchRequestRef.current) {
searchRequestRef.current.abort();
}
if (value.trim().length === 0) {
setSearching(false);
return;
}
setLoading(true);
searchRequestRef.current = new AbortController();
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
signal: searchRequestRef.current.signal,
params: {
q: value,
resolve: false,
following: true,
},
})
.then((data) => {
dispatch(importFetchedAccounts(data));
setSearchAccountIds(data.map((a) => a.id));
setLoading(false);
setSearching(true);
return '';
})
.catch(() => {
setSearching(true);
setLoading(false);
});
},
500,
{ leading: true, trailing: true },
);
let displayedAccountIds: string[];
if (mode === 'add') {
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
} else {
displayedAccountIds = accountIds;
}
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(messages.heading)}
>
{mode === 'remove' ? (
<ColumnHeader
title={intl.formatMessage(messages.heading)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
extraButton={
<button
onClick={handleSearchClick}
type='button'
className='column-header__button'
title={intl.formatMessage(messages.enterSearch)}
aria-label={intl.formatMessage(messages.enterSearch)}
>
<Icon id='plus' icon={AddIcon} />
</button>
}
/>
) : (
<ColumnSearchHeader
onBack={handleDismissSearchClick}
onSubmit={handleSearch}
/>
)}
<ScrollableList
scrollKey='list_members'
trackScroll={!multiColumn}
bindToDocument={!multiColumn}
isLoading={loading}
showLoading={loading && displayedAccountIds.length === 0}
hasMore={false}
footer={
mode === 'remove' && (
<>
{displayedAccountIds.length > 0 && <div className='spacer' />}
<div className='column-footer'>
<Link to={`/lists/${id}`} className='button button--block'>
<FormattedMessage id='lists.done' defaultMessage='Done' />
</Link>
</div>
</>
)
}
emptyMessage={
mode === 'remove' ? (
<>
<span>
<FormattedMessage
id='lists.no_members_yet'
defaultMessage='No members yet.'
/>
<br />
<FormattedMessage
id='lists.find_users_to_add'
defaultMessage='Find users to add'
/>
</span>
<SquigglyArrow className='empty-column-indicator__arrow' />
</>
) : (
<FormattedMessage
id='lists.no_results_found'
defaultMessage='No results found.'
/>
)
}
>
{displayedAccountIds.map((accountId) => (
<AccountItem
key={accountId}
accountId={accountId}
listId={id}
partOfList={
displayedAccountIds === accountIds ||
accountIds.includes(accountId)
}
onToggle={handleAccountToggle}
/>
))}
</ScrollableList>
<Helmet>
<title>{intl.formatMessage(messages.heading)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default ListMembers;

View file

@ -0,0 +1,342 @@
import { useCallback, useState, useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useParams, useHistory, Link } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit';
import Toggle from 'react-toggle';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'mastodon/actions/lists';
import { createList, updateList } from 'mastodon/actions/lists_typed';
import { apiGetAccounts } from 'mastodon/api/lists';
import type { RepliesPolicyType } from 'mastodon/api_types/lists';
import Column from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
edit: { id: 'column.edit_list', defaultMessage: 'Edit list' },
create: { id: 'column.create_list', defaultMessage: 'Create list' },
});
const MembersLink: React.FC<{
id: string;
}> = ({ id }) => {
const [count, setCount] = useState(0);
const [avatars, setAvatars] = useState<string[]>([]);
useEffect(() => {
void apiGetAccounts(id)
.then((data) => {
setCount(data.length);
setAvatars(data.slice(0, 3).map((a) => a.avatar));
return '';
})
.catch(() => {
// Nothing
});
}, [id, setCount, setAvatars]);
return (
<Link to={`/lists/${id}/members`} className='app-form__link'>
<div className='app-form__link__text'>
<strong>
<FormattedMessage
id='lists.list_members'
defaultMessage='List members'
/>
</strong>
<FormattedMessage
id='lists.list_members_count'
defaultMessage='{count, plural, one {# member} other {# members}}'
values={{ count }}
/>
</div>
<div className='avatar-pile'>
{avatars.map((url) => (
<img key={url} src={url} alt='' />
))}
</div>
</Link>
);
};
const NewList: React.FC<{
multiColumn?: boolean;
}> = ({ multiColumn }) => {
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 [notify, setNotify] = useState(false);
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);
setNotify(list.notify);
}
}, [setTitle, setExclusive, setRepliesPolicy, setNotify, id, list]);
const handleTitleChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
setTitle(value);
},
[setTitle],
);
const handleExclusiveChange = useCallback(
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
setExclusive(checked);
},
[setExclusive],
);
const handleRepliesPolicyChange = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
setRepliesPolicy(value as RepliesPolicyType);
},
[setRepliesPolicy],
);
const handleNotifyChange = useCallback(
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
setNotify(checked);
},
[setNotify],
);
const handleSubmit = useCallback(() => {
setSubmitting(true);
if (id) {
void dispatch(
updateList({
id,
title,
exclusive,
replies_policy: repliesPolicy,
notify,
}),
).then(() => {
setSubmitting(false);
return '';
});
} else {
void dispatch(
createList({
title,
exclusive,
replies_policy: repliesPolicy,
notify,
}),
).then((result) => {
setSubmitting(false);
if (isFulfilled(result)) {
history.replace(`/lists/${result.payload.id}/edit`);
history.push(`/lists/${result.payload.id}/members`);
}
return '';
});
}
}, [
history,
dispatch,
setSubmitting,
id,
title,
exclusive,
repliesPolicy,
notify,
]);
return (
<Column
bindToDocument={!multiColumn}
label={intl.formatMessage(id ? messages.edit : messages.create)}
>
<ColumnHeader
title={intl.formatMessage(id ? messages.edit : messages.create)}
icon='list-ul'
iconComponent={ListAltIcon}
multiColumn={multiColumn}
showBackButton
/>
<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='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.notify'
defaultMessage='Notify list'
/>
</strong>
<span className='hint'>
<FormattedMessage
id='lists.notify_hint'
defaultMessage='Notify when new post is added.'
/>
</span>
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={notify} onChange={handleNotifyChange} />
</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>
</div>
<Helmet>
<title>
{intl.formatMessage(id ? messages.edit : messages.create)}
</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default NewList;