1
0
Fork 0
forked from gitea/nas

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

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

View file

@ -1,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.map.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));

View file

@ -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 CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Icon } from 'mastodon/components/icon';
import { removeFromCircleAdder, addToCircleAdder } from '../../../actions/circles';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
remove: { id: 'circles.account.remove', defaultMessage: 'Remove from circle' },
add: { id: 'circles.account.add', defaultMessage: 'Add to circle' },
});
const MapStateToProps = (state, { circleId, added }) => ({
circle: state.get('circles').get(circleId),
added: typeof added === 'undefined' ? state.getIn(['circleAdder', 'circles', 'items']).includes(circleId) : added,
});
const mapDispatchToProps = (dispatch, { circleId }) => ({
onRemove: () => dispatch(removeFromCircleAdder(circleId)),
onAdd: () => dispatch(addToCircleAdder(circleId)),
});
class Circle extends ImmutablePureComponent {
static propTypes = {
circle: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
added: PropTypes.bool,
};
static defaultProps = {
added: false,
};
render () {
const { circle, 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='user-circle' icon={CircleIcon} className='column-link__icon' fixedWidth />
{circle.get('title')}
</div>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(Circle));

View file

@ -1,77 +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 { setupCircleAdder, resetCircleAdder } from '../../actions/circles';
import NewCircleForm from '../circles/components/new_circle_form';
import Account from './components/account';
import Circle from './components/circle';
// hack
const getOrderedCircles = createSelector([state => state.get('circles')], circles => {
if (!circles) {
return circles;
}
return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
circleIds: getOrderedCircles(state).map(circle=>circle.get('id')),
});
const mapDispatchToProps = dispatch => ({
onInitialize: accountId => dispatch(setupCircleAdder(accountId)),
onReset: () => dispatch(resetCircleAdder()),
});
class CircleAdder extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onInitialize: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
circleIds: ImmutablePropTypes.list.isRequired,
};
componentDidMount () {
const { onInitialize, accountId } = this.props;
onInitialize(accountId);
}
componentWillUnmount () {
const { onReset } = this.props;
onReset();
}
render () {
const { accountId, circleIds } = this.props;
return (
<div className='modal-root__modal list-adder'>
<div className='list-adder__account'>
<Account accountId={accountId} />
</div>
<NewCircleForm />
<div className='list-adder__lists'>
{circleIds.map(CircleId => <Circle key={CircleId} circleId={CircleId} />)}
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(CircleAdder));

View file

@ -0,0 +1,213 @@
import { useEffect, useState, useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { isFulfilled } from '@reduxjs/toolkit';
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { fetchCircles } from 'mastodon/actions/circles';
import { createCircle } from 'mastodon/actions/circles_typed';
import {
apiGetAccountCircles,
apiAddAccountToCircle,
apiRemoveAccountFromCircle,
} from 'mastodon/api/circles';
import type { ApiCircleJSON } from 'mastodon/api_types/circles';
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 { getOrderedCircles } from 'mastodon/selectors/circles';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
newCircle: {
id: 'circles.new_circle_name',
defaultMessage: 'New circle name',
},
createCircle: {
id: 'circles.create',
defaultMessage: 'Create',
},
close: {
id: 'lightbox.close',
defaultMessage: 'Close',
},
});
const CircleItem: 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='circle-ul' icon={CircleIcon} />
<span>{title}</span>
</div>
<CheckBox value={id} checked={checked} onChange={handleChange} />
</label>
);
};
const NewCircleItem: React.FC<{
onCreate: (circle: ApiCircleJSON) => 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(createCircle({ 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='circle-ul' icon={CircleIcon} />
<input
type='text'
value={title}
onChange={handleChange}
maxLength={30}
required
placeholder={intl.formatMessage(messages.newCircle)}
/>
</label>
<Button text={intl.formatMessage(messages.createCircle)} type='submit' />
</form>
);
};
const CircleAdder: React.FC<{
accountId: string;
onClose: () => void;
}> = ({ accountId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const circles = useAppSelector((state) => getOrderedCircles(state));
const [circleIds, setCircleIds] = useState<string[]>([]);
useEffect(() => {
dispatch(fetchCircles());
apiGetAccountCircles(accountId)
.then((data) => {
setCircleIds(data.map((l) => l.id));
return '';
})
.catch(() => {
// Nothing
});
}, [dispatch, setCircleIds, accountId]);
const handleToggle = useCallback(
(circleId: string, checked: boolean) => {
if (checked) {
setCircleIds((currentCircleIds) => [circleId, ...currentCircleIds]);
apiAddAccountToCircle(circleId, accountId).catch(() => {
setCircleIds((currentCircleIds) =>
currentCircleIds.filter((id) => id !== circleId),
);
});
} else {
setCircleIds((currentCircleIds) =>
currentCircleIds.filter((id) => id !== circleId),
);
apiRemoveAccountFromCircle(circleId, accountId).catch(() => {
setCircleIds((currentCircleIds) => [circleId, ...currentCircleIds]);
});
}
},
[setCircleIds, accountId],
);
const handleCreate = useCallback(
(circle: ApiCircleJSON) => {
setCircleIds((currentCircleIds) => [circle.id, ...currentCircleIds]);
apiAddAccountToCircle(circle.id, accountId).catch(() => {
setCircleIds((currentCircleIds) =>
currentCircleIds.filter((id) => id !== circle.id),
);
});
},
[setCircleIds, 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='circles.add_to_circles'
defaultMessage='Add {name} to circles'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
</span>
</div>
<div className='dialog-modal__content'>
<div className='lists-scrollable'>
<NewCircleItem onCreate={handleCreate} />
{circles.map((circle) => (
<CircleItem
key={circle.id}
id={circle.id}
title={circle.title}
checked={circleIds.includes(circle.id)}
onChange={handleToggle}
/>
))}
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default CircleAdder;