Merge commit 'd0753ec276502ea28a1e853cbc6487020a0602e3' into glitch-soc/merge-upstream
This commit is contained in:
commit
f740611820
169 files changed
+2145
-2356
No files matched your search
@@ -1,43 +0,0 @@
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
||||
@@ -1,75 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { listId, added }) => ({
|
||||
list: state.get('lists').get(listId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { listId }) => ({
|
||||
onRemove: () => dispatch(removeFromListAdder(listId)),
|
||||
onAdd: () => dispatch(addToListAdder(listId)),
|
||||
});
|
||||
|
||||
class List extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
list: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { list, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} className='column-link__icon' />
|
||||
{list.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
|
||||
import Account from './components/account';
|
||||
import List from './components/list';
|
||||
// hack
|
||||
|
||||
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 => ({
|
||||
listIds: getOrderedLists(state).map(list=>list.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
||||
onReset: () => dispatch(resetListAdder()),
|
||||
});
|
||||
|
||||
class ListAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
listIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountId, listIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewListForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListAdder));
|
||||
@@ -0,0 +1,213 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { createList } from 'mastodon/actions/lists_typed';
|
||||
import {
|
||||
apiGetAccountLists,
|
||||
apiAddAccountToList,
|
||||
apiRemoveAccountFromList,
|
||||
} from 'mastodon/api/lists';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newList: {
|
||||
id: 'lists.new_list_name',
|
||||
defaultMessage: 'New list name',
|
||||
},
|
||||
createList: {
|
||||
id: 'lists.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const ListItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewListItem: React.FC<{
|
||||
onCreate: (list: ApiListJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createList({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='lists__item' onSubmit={handleSubmit}>
|
||||
<label className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newList)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button text={intl.formatMessage(messages.createList)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ListAdder: React.FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
const [listIds, setListIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLists());
|
||||
|
||||
apiGetAccountLists(accountId)
|
||||
.then((data) => {
|
||||
setListIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setListIds, accountId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(listId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
|
||||
apiRemoveAccountFromList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(list: ApiListJSON) => {
|
||||
setListIds((currentListIds) => [list.id, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(list.id, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== list.id),
|
||||
);
|
||||
});
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='lists.add_to_lists'
|
||||
defaultMessage='Add {name} to lists'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='lists-scrollable'>
|
||||
<NewListItem onCreate={handleCreate} />
|
||||
|
||||
{lists.map((list) => (
|
||||
<ListItem
|
||||
key={list.id}
|
||||
id={list.id}
|
||||
title={list.title}
|
||||
checked={listIds.includes(list.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ListAdder;
|
||||
@@ -1,82 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromListEditor(accountId)),
|
||||
onAdd: () => dispatch(addToListEditor(accountId)),
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(false)),
|
||||
});
|
||||
|
||||
class ListForm 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 title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListForm));
|
||||
@@ -1,83 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchListSuggestions(value)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onChange: value => dispatch(changeListSuggestions(value)),
|
||||
});
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
||||
@@ -1,83 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import Account from './components/account';
|
||||
import EditListForm from './components/edit_list_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditListForm />
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListEditor));
|
||||
@@ -1,21 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { fetchList, updateList } from 'mastodon/actions/lists';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { connectListStream } from 'mastodon/actions/streaming';
|
||||
import { expandListTimeline } from 'mastodon/actions/timelines';
|
||||
@@ -23,17 +21,10 @@ 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 { RadioButton } from 'mastodon/components/radio_button';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
const messages = defineMessages({
|
||||
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||
@@ -115,13 +106,6 @@ class ListTimeline extends PureComponent {
|
||||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'LIST_EDITOR',
|
||||
modalProps: { listId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
const { id } = this.props.params;
|
||||
@@ -129,25 +113,11 @@ class ListTimeline extends PureComponent {
|
||||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
||||
};
|
||||
|
||||
handleRepliesPolicyChange = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, undefined, target.value));
|
||||
};
|
||||
|
||||
onExclusiveToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
|
||||
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
@@ -178,35 +148,14 @@ class ListTimeline extends PureComponent {
|
||||
>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{replies_policy !== undefined && (
|
||||
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
{ ['none', 'list', 'followed'].map(policy => (
|
||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
@@ -229,4 +178,4 @@ class ListTimeline extends PureComponent {
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
|
||||
export default withRouter(connect(mapStateToProps)(ListTimeline));
|
||||
@@ -1,80 +0,0 @@
|
||||
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));
|
||||
@@ -1,94 +0,0 @@
|
||||
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' },
|
||||
});
|
||||
|
||||
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')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Lists));
|
||||
@@ -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;
|
||||
@@ -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' && (
|
||||
<>
|
||||
<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;
|
||||
@@ -0,0 +1,296 @@
|
||||
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 [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);
|
||||
},
|
||||
[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 handleSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (id) {
|
||||
void dispatch(
|
||||
updateList({
|
||||
id,
|
||||
title,
|
||||
exclusive,
|
||||
replies_policy: repliesPolicy,
|
||||
}),
|
||||
).then(() => {
|
||||
setSubmitting(false);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(
|
||||
createList({
|
||||
title,
|
||||
exclusive,
|
||||
replies_policy: repliesPolicy,
|
||||
}),
|
||||
).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]);
|
||||
|
||||
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='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;
|
||||
@@ -43,7 +43,7 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||
({ clientX, clientY, target, button }) => {
|
||||
({ clientX, clientY, target, button, ctrlKey, metaKey }) => {
|
||||
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
|
||||
const [deltaX, deltaY] = [
|
||||
Math.abs(clientX - startX),
|
||||
@@ -64,8 +64,14 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
element = element.parentNode as HTMLDivElement | null;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && button === 0 && account) {
|
||||
history.push(`/@${account.acct}/${statusId}`);
|
||||
if (deltaX + deltaY < 5 && account) {
|
||||
const path = `/@${account.acct}/${statusId}`;
|
||||
|
||||
if (button === 0 && !(ctrlKey || metaKey)) {
|
||||
history.push(path);
|
||||
} else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) {
|
||||
window.open(path, '_blank', 'noreferrer noopener');
|
||||
}
|
||||
}
|
||||
|
||||
clickCoordinatesRef.current = null;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
DomainBlockModal,
|
||||
ReportModal,
|
||||
EmbedModal,
|
||||
ListEditor,
|
||||
ListAdder,
|
||||
CompareHistoryModal,
|
||||
FilterModal,
|
||||
@@ -64,7 +63,6 @@ export const MODAL_COMPONENTS = {
|
||||
'REPORT': ReportModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'LIST_ADDER': ListAdder,
|
||||
'COMPARE_HISTORY': CompareHistoryModal,
|
||||
|
||||
@@ -58,11 +58,13 @@ import {
|
||||
FollowedTags,
|
||||
LinkTimeline,
|
||||
ListTimeline,
|
||||
Lists,
|
||||
ListEdit,
|
||||
ListMembers,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
Mutes,
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Directory,
|
||||
Explore,
|
||||
Onboarding,
|
||||
@@ -205,6 +207,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
|
||||
<WrappedRoute path='/lists/new' component={ListEdit} content={children} />
|
||||
<WrappedRoute path='/lists/:id/edit' component={ListEdit} content={children} />
|
||||
<WrappedRoute path='/lists/:id/members' component={ListMembers} content={children} />
|
||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||
|
||||
@@ -150,10 +150,6 @@ export function EmbedModal () {
|
||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
||||
}
|
||||
|
||||
export function ListEditor () {
|
||||
return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
|
||||
}
|
||||
|
||||
export function ListAdder () {
|
||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
||||
}
|
||||
@@ -221,3 +217,11 @@ export function LinkTimeline () {
|
||||
export function AnnualReportModal () {
|
||||
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal');
|
||||
}
|
||||
|
||||
export function ListEdit () {
|
||||
return import(/*webpackChunkName: "features/lists" */'../../lists/new');
|
||||
}
|
||||
|
||||
export function ListMembers () {
|
||||
return import(/* webpackChunkName: "features/lists" */'../../lists/members');
|
||||
}
|
||||
Reference in new issue
Block a user