Merge branch 'ts' into 'develop'

Lists modals: TypeScript, FC

See merge request soapbox-pub/soapbox-fe!1490
environments/review-develop-3zknud/deployments/167
marcin mikołajczak 2 years ago
commit 8aceefbbe8

@ -1,44 +0,0 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
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;
};
export default @connect(makeMapStateToProps)
@injectIntl
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>
);
}
}

@ -0,0 +1,31 @@
import React from 'react';
import DisplayName from 'soapbox/components/display-name';
import { Avatar } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const getAccount = makeGetAccount();
interface IAccount {
accountId: string,
}
const Account: React.FC<IAccount> = ({ accountId }) => {
const account = useAppSelector((state) => getAccount(state, accountId));
if (!account) return null;
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Avatar src={account.avatar} size={36} /></div>
<DisplayName account={account} />
</div>
</div>
</div>
);
};
export default Account;

@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { removeFromListAdder, addToListAdder } from 'soapbox/actions/lists';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/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)),
});
export default @connect(MapStateToProps, mapDispatchToProps)
@injectIntl
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 src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
}
return (
<div className='list'>
<div className='list__wrapper'>
<div className='list__display-name'>
<Icon id='list-ul' className='column-link__icon' fixedWidth />
{list.get('title')}
</div>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}

@ -0,0 +1,49 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { removeFromListAdder, addToListAdder } from 'soapbox/actions/lists';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
});
interface IList {
listId: string,
}
const List: React.FC<IList> = ({ listId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const list = useAppSelector((state) => state.lists.get(listId));
const added = useAppSelector((state) => state.listAdder.lists.items.includes(listId));
const onRemove = () => dispatch(removeFromListAdder(listId));
const onAdd = () => dispatch(addToListAdder(listId));
if (!list) return null;
let button;
if (added) {
button = <IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
}
return (
<div className='flex items-center gap-1.5 px-2 py-4 text-black dark:text-white'>
<Icon src={require('@tabler/icons/icons/list.svg')} fixedWidth />
<span className='flex-grow'>
{list.title}
</span>
{button}
</div>
);
};
export default List;

@ -1,105 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setupListAdder, resetListAdder } from 'soapbox/actions/lists';
import { CardHeader, CardTitle, Modal } from 'soapbox/components/ui';
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, { accountId }) => ({
listIds: getOrderedLists(state).map(list=>list.get('id')),
account: state.getIn(['accounts', accountId]),
});
const mapDispatchToProps = dispatch => ({
onInitialize: accountId => dispatch(setupListAdder(accountId)),
onReset: () => dispatch(resetListAdder()),
});
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
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,
account: ImmutablePropTypes.record,
};
componentDidMount() {
const { onInitialize, accountId } = this.props;
onInitialize(accountId);
}
componentWillUnmount() {
const { onReset } = this.props;
onReset();
}
onClickClose = () => {
this.props.onClose('LIST_ADDER');
};
render() {
const { accountId, listIds, intl } = this.props;
return (
<Modal
title={<FormattedMessage id='list_adder.header_title' defaultMessage='Add or Remove from Lists' />}
onClose={this.onClickClose}
>
<div className='compose-modal__content'>
<div className='list-adder'>
<div className='list-adder__account'>
<Account accountId={accountId} />
</div>
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.add)} />
</CardHeader>
<NewListForm />
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading)} />
</CardHeader>
<div className='list-adder__lists'>
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
</div>
</div>
</div>
</Modal>
);
}
}

@ -0,0 +1,83 @@
import React from 'react';
import { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import { setupListAdder, resetListAdder } from 'soapbox/actions/lists';
import { CardHeader, CardTitle, Modal } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import NewListForm from '../lists/components/new_list_form';
import Account from './components/account';
import List from './components/list';
import type { List as ImmutableList } from 'immutable';
import type { RootState } from 'soapbox/store';
import type { List as ListEntity } from 'soapbox/types/entities';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
});
// hack
const getOrderedLists = createSelector([(state: RootState) => state.lists], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => (a as ListEntity).title.localeCompare((b as ListEntity).title)) as ImmutableList<ListEntity>;
});
interface IListAdder {
accountId: string,
onClose: (type: string) => void,
}
const ListAdder: React.FC<IListAdder> = ({ accountId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const listIds = useAppSelector((state) => getOrderedLists(state).map(list => list.id));
useEffect(() => {
dispatch(setupListAdder(accountId));
return () => {
dispatch(resetListAdder());
};
}, []);
const onClickClose = () => {
onClose('LIST_ADDER');
};
return (
<Modal
title={<FormattedMessage id='list_adder.header_title' defaultMessage='Add or Remove from Lists' />}
onClose={onClickClose}
>
<Account accountId={accountId} />
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.add)} />
</CardHeader>
<NewListForm />
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading)} />
</CardHeader>
<div>
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
</div>
</Modal>
);
};
export default ListAdder;

@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
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)),
});
export default @connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
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 src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} 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>
);
}
}

@ -0,0 +1,58 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { removeFromListEditor, addToListEditor } from 'soapbox/actions/lists';
import DisplayName from 'soapbox/components/display-name';
import IconButton from 'soapbox/components/icon_button';
import { Avatar } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
});
const getAccount = makeGetAccount();
interface IAccount {
accountId: string,
}
const Account: React.FC<IAccount> = ({ accountId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => getAccount(state, accountId));
const isAdded = useAppSelector((state) => state.listEditor.accounts.items.includes(accountId));
const onRemove = () => dispatch(removeFromListEditor(accountId));
const onAdd = () => dispatch(addToListEditor(accountId));
if (!account) return null;
let button;
if (isAdded) {
button = <IconButton src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} 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 src={account.avatar} size={36} /></div>
<DisplayName account={account} />
</div>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
};
export default Account;

@ -1,73 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import { Button } from '../../../components/ui';
const messages = defineMessages({
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
save: { id: 'lists.new.save_title', defaultMessage: 'Save Title' },
});
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'title']),
disabled: !state.getIn(['listEditor', 'isChanged']),
});
const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeListEditorTitle(value)),
onSubmit: () => dispatch(submitListEditor(false)),
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class ListForm extends React.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 save = intl.formatMessage(messages.save);
return (
<form className='column-inline-form' method='post' onSubmit={this.handleSubmit}>
<input
className='setting-text new-list-form__input'
value={value}
onChange={this.handleChange}
/>
{ !disabled &&
<Button
className='new-list-form__btn'
onClick={this.handleClick}
>
{save}
</Button>
}
</form>
);
}
}

@ -0,0 +1,53 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { changeListEditorTitle, submitListEditor } from 'soapbox/actions/lists';
import { Button, Form, HStack, Input } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
save: { id: 'lists.new.save_title', defaultMessage: 'Save Title' },
});
const ListForm = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const value = useAppSelector((state) => state.listEditor.title);
const disabled = useAppSelector((state) => !state.listEditor.isChanged);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = e => {
dispatch(changeListEditorTitle(e.target.value));
};
const handleSubmit: React.FormEventHandler<Element> = e => {
e.preventDefault();
dispatch(submitListEditor(false));
};
const handleClick = () => {
dispatch(submitListEditor(false));
};
const save = intl.formatMessage(messages.save);
return (
<Form onSubmit={handleSubmit}>
<HStack space={2}>
<Input
outerClassName='flex-grow'
type='text'
value={value}
onChange={handleChange}
/>
<Button onClick={handleClick} disabled={disabled}>
{save}
</Button>
</HStack>
</Form>
);
};
export default ListForm;

@ -1,83 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from 'soapbox/actions/lists';
import Icon from 'soapbox/components/icon';
import { Button } from 'soapbox/components/ui';
const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
searchTitle: { id: 'tabs_bar.search', defaultMessage: 'Search' },
});
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)),
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class Search extends React.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);
}
}
handleSubmit = () => {
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 src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
</div>
<Button onClick={this.handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
</div>
);
}
}

@ -0,0 +1,58 @@
import classNames from 'classnames';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from 'soapbox/actions/lists';
import Icon from 'soapbox/components/icon';
import { Button, Form, HStack, Input } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
searchTitle: { id: 'tabs_bar.search', defaultMessage: 'Search' },
});
const Search = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const value = useAppSelector((state) => state.listEditor.suggestions.value);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = e => {
dispatch(changeListSuggestions(e.target.value));
};
const handleSubmit = () => {
dispatch(fetchListSuggestions(value));
};
const handleClear = () => {
dispatch(clearListSuggestions());
};
const hasValue = value.length > 0;
return (
<Form onSubmit={handleSubmit}>
<HStack space={2}>
<label className='flex-grow relative'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<Input
type='text'
value={value}
onChange={handleChange}
placeholder={intl.formatMessage(messages.search)}
/>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
</div>
</label>
<Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
</HStack>
</Form>
);
};
export default Search;

@ -1,105 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { setupListEditor, clearListSuggestions, resetListEditor } from 'soapbox/actions/lists';
import { CardHeader, CardTitle, Modal } from 'soapbox/components/ui';
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()),
});
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
changeTitle: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
editList: { id: 'lists.edit', defaultMessage: 'Edit list' },
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
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();
}
onClickClose = () => {
this.props.onClose('LIST_ADDER');
};
render() {
const { accountIds, searchAccountIds, intl } = this.props;
return (
<Modal
title={<FormattedMessage id='lists.edit' defaultMessage='Edit list' />}
onClose={this.onClickClose}
>
<div className='compose-modal__content list-editor__content'>
<div className='list-editor'>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.changeTitle)} />
</CardHeader>
<EditListForm />
<br />
{
accountIds.size > 0 &&
<div>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.removeFromList)} />
</CardHeader>
<div className='list-editor__accounts'>
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
</div>
</div>
}
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.addToList)} />
</CardHeader>
<Search />
<div className='list-editor__accounts'>
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>
</div>
</div>
</Modal>
);
}
}

@ -0,0 +1,79 @@
import React from 'react';
import { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { setupListEditor, resetListEditor } from 'soapbox/actions/lists';
import { CardHeader, CardTitle, Modal } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import Account from './components/account';
import EditListForm from './components/edit_list_form';
import Search from './components/search';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
changeTitle: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
editList: { id: 'lists.edit', defaultMessage: 'Edit list' },
});
interface IListEditor {
listId: string,
onClose: (type: string) => void,
}
const ListEditor: React.FC<IListEditor> = ({ listId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.listEditor.accounts.items);
const searchAccountIds = useAppSelector((state) => state.listEditor.suggestions.items);
useEffect(() => {
dispatch(setupListEditor(listId));
return () => {
dispatch(resetListEditor());
};
}, []);
const onClickClose = () => {
onClose('LIST_ADDER');
};
return (
<Modal
title={<FormattedMessage id='lists.edit' defaultMessage='Edit list' />}
onClose={onClickClose}
>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.changeTitle)} />
</CardHeader>
<EditListForm />
<br />
{accountIds.size > 0 && (
<div>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.removeFromList)} />
</CardHeader>
<div className='max-h-48 overflow-y-auto'>
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>
</div>
)}
<br />
<CardHeader>
<CardTitle title={intl.formatMessage(messages.addToList)} />
</CardHeader>
<Search />
<div className='max-h-48 overflow-y-auto'>
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>
</Modal>
);
};
export default ListEditor;

@ -65,7 +65,7 @@ const ListTimeline: React.FC = () => {
// })); // }));
// }; // };
const title = list ? list.get('title') : id; const title = list ? list.title : id;
if (typeof list === 'undefined') { if (typeof list === 'undefined') {
return ( return (

@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { changeListEditorTitle, submitListEditor } from 'soapbox/actions/lists'; import { changeListEditorTitle, submitListEditor } from 'soapbox/actions/lists';
import { Button } from 'soapbox/components/ui'; import { Button, Form, HStack, Input } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks'; import { useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({ const messages = defineMessages({
@ -23,7 +23,7 @@ const NewListForm: React.FC = () => {
dispatch(changeListEditorTitle(e.target.value)); dispatch(changeListEditorTitle(e.target.value));
}; };
const handleSubmit = (e: React.FormEvent<HTMLFormElement | HTMLButtonElement>) => { const handleSubmit = (e: React.FormEvent<Element>) => {
e.preventDefault(); e.preventDefault();
dispatch(submitListEditor(true)); dispatch(submitListEditor(true));
}; };
@ -32,27 +32,28 @@ const NewListForm: React.FC = () => {
const create = intl.formatMessage(messages.create); const create = intl.formatMessage(messages.create);
return ( return (
<form className='column-inline-form' method='post' onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<label> <HStack space={2}>
<span style={{ display: 'none' }}>{label}</span> <label className='flex-grow'>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text new-list-form__input' <Input
value={value} type='text'
value={value}
disabled={disabled}
onChange={handleChange}
placeholder={label}
/>
</label>
<Button
disabled={disabled} disabled={disabled}
onChange={handleChange} onClick={handleSubmit}
placeholder={label} >
/> {create}
</label> </Button>
</HStack>
<Button </Form>
// className='new-list-form__btn'
disabled={disabled}
onClick={handleSubmit}
>
{create}
</Button>
</form>
); );
}; };

@ -93,13 +93,13 @@ const Lists: React.FC = () => {
itemClassName='py-2' itemClassName='py-2'
> >
{lists.map((list: any) => ( {lists.map((list: any) => (
<Link key={list.get('id')} to={`/list/${list.get('id')}`} className='flex items-center gap-1.5 p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg'> <Link key={list.id} to={`/list/${list.id}`} className='flex items-center gap-1.5 p-2 text-black dark:text-white hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg'>
<Icon src={require('@tabler/icons/icons/list.svg')} fixedWidth /> <Icon src={require('@tabler/icons/icons/list.svg')} fixedWidth />
<span className='flex-grow'> <span className='flex-grow'>
{list.get('title')} {list.title}
</span> </span>
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/pencil.svg')} onClick={handleEditClick(list.get('id'))} title={intl.formatMessage(messages.editList)} /> <IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/pencil.svg')} onClick={handleEditClick(list.id)} title={intl.formatMessage(messages.editList)} />
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/trash.svg')} onClick={handleDeleteClick(list.get('id'))} title={intl.formatMessage(messages.deleteList)} /> <IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/trash.svg')} onClick={handleDeleteClick(list.id)} title={intl.formatMessage(messages.deleteList)} />
</Link> </Link>
))} ))}
</ScrollableList> </ScrollableList>

@ -30,7 +30,7 @@ const ColorPicker: React.FC<IColorPicker> = ({ style, value, onClose, onChange }
document.removeEventListener('click', handleDocumentClick, false); document.removeEventListener('click', handleDocumentClick, false);
document.removeEventListener('touchend', handleDocumentClick); document.removeEventListener('touchend', handleDocumentClick);
}; };
}); }, []);
const pickerStyle: React.CSSProperties = { const pickerStyle: React.CSSProperties = {
...style, ...style,

@ -9,6 +9,7 @@ import {
EmojiRecord, EmojiRecord,
FieldRecord, FieldRecord,
InstanceRecord, InstanceRecord,
ListRecord,
MentionRecord, MentionRecord,
NotificationRecord, NotificationRecord,
PollRecord, PollRecord,
@ -28,6 +29,7 @@ type ChatMessage = ReturnType<typeof ChatMessageRecord>;
type Emoji = ReturnType<typeof EmojiRecord>; type Emoji = ReturnType<typeof EmojiRecord>;
type Field = ReturnType<typeof FieldRecord>; type Field = ReturnType<typeof FieldRecord>;
type Instance = ReturnType<typeof InstanceRecord>; type Instance = ReturnType<typeof InstanceRecord>;
type List = ReturnType<typeof ListRecord>;
type Mention = ReturnType<typeof MentionRecord>; type Mention = ReturnType<typeof MentionRecord>;
type Notification = ReturnType<typeof NotificationRecord>; type Notification = ReturnType<typeof NotificationRecord>;
type Poll = ReturnType<typeof PollRecord>; type Poll = ReturnType<typeof PollRecord>;
@ -61,6 +63,7 @@ export {
Emoji, Emoji,
Field, Field,
Instance, Instance,
List,
Mention, Mention,
Notification, Notification,
Poll, Poll,

@ -44,7 +44,6 @@
@import 'components/status'; @import 'components/status';
@import 'components/reply-mentions'; @import 'components/reply-mentions';
@import 'components/detailed-status'; @import 'components/detailed-status';
@import 'components/list-forms';
@import 'components/media-gallery'; @import 'components/media-gallery';
@import 'components/notification'; @import 'components/notification';
@import 'components/display-name'; @import 'components/display-name';

@ -1,105 +0,0 @@
.list-editor {
flex-direction: column;
width: 100%;
overflow: hidden;
height: 100%;
overflow-y: auto;
h4 {
padding: 15px 0;
background: var(--background-color);
font-weight: 500;
font-size: 16px;
text-align: center;
border-radius: 8px 8px 0 0;
}
&__content {
padding: 0;
}
&__accounts {
background: var(--background-color);
overflow-y: auto;
max-height: 200px;
}
.account__display-name {
&:hover strong {
text-decoration: none;
}
}
.account__avatar {
cursor: default;
}
.search {
display: flex;
flex-direction: row;
margin: 10px;
> label {
flex: 1 1;
}
> .search__icon {
position: relative;
}
> .button {
width: 80px;
margin-left: 10px;
}
}
}
.list-adder {
flex-direction: column;
width: 100%;
overflow: hidden;
height: 100%;
overflow-y: auto;
&__account {
background: var(--background-color);
border-radius: 4px;
}
&__lists {
background: var(--background-color);
}
.list {
padding: 4px;
border-bottom: 1px solid var(--brand-color--med);
}
.list__wrapper {
display: flex;
.account__relationship {
padding: 8px 5px 0;
}
}
.list__display-name {
flex: 1 1 auto;
overflow: hidden;
text-decoration: none;
font-size: 16px;
padding: 10px;
}
}
.new-list-form,
.edit-list-form {
&__btn {
margin-left: 6px;
width: 112px;
}
&__input {
height: 36px;
}
}

@ -339,33 +339,6 @@
} }
} }
.compose-modal__content {
display: flex;
flex-direction: row;
flex: 1;
overflow-y: hidden;
&--scroll {
display: block;
overflow-y: auto;
}
.timeline-compose-block {
background: transparent !important;
width: 100%;
padding: 0;
margin-bottom: 0;
.compose-form {
max-height: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
padding: 0 !important;
}
}
}
.reply-mentions-modal__accounts { .reply-mentions-modal__accounts {
display: block; display: block;
flex-direction: row; flex-direction: row;

@ -118,13 +118,3 @@ input.search__input {
right: 24px; right: 24px;
} }
} }
.list-editor__search {
.search__icon {
position: relative;
.svg-icon {
right: 0;
}
}
}

Loading…
Cancel
Save