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,96 +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 AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { Icon } from 'mastodon/components/icon';
import { removeFromAntennaAdder, addToAntennaAdder, removeExcludeFromAntennaAdder, addExcludeToAntennaAdder } from '../../../actions/antennas';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
});
const MapStateToProps = (state, { antennaId, added }) => ({
antenna: state.get('antennas').get(antennaId),
added: typeof added === 'undefined' ? state.getIn(['antennaAdder', 'antennas', 'items']).includes(antennaId) : added,
});
const mapDispatchToProps = (dispatch, { antennaId }) => ({
onRemove: () => dispatch(removeFromAntennaAdder(antennaId)),
onAdd: () => dispatch(addToAntennaAdder(antennaId)),
onExcludeRemove: () => dispatch(removeExcludeFromAntennaAdder(antennaId)),
onExcludeAdd: () => dispatch(addExcludeToAntennaAdder(antennaId)),
});
class Antenna extends ImmutablePureComponent {
static propTypes = {
antenna: ImmutablePropTypes.map.isRequired,
isExclude: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
onExcludeRemove: PropTypes.func.isRequired,
onExcludeAdd: PropTypes.func.isRequired,
added: PropTypes.bool,
};
static defaultProps = {
added: false,
};
handleRemove = () => {
if (this.props.isExclude) {
this.props.onExcludeRemove();
} else {
this.props.onRemove();
}
};
handleAdd = () => {
if (this.props.isExclude) {
this.props.onExcludeAdd();
} else {
this.props.onAdd();
}
};
render () {
const { antenna, intl, added } = this.props;
let button;
if (added) {
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={this.handleRemove} />;
} else {
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={this.handleAdd} />;
}
return (
<div className='list'>
<div className='list__wrapper'>
<div className='list__display-name'>
<Icon id='wifi' icon={AntennaIcon} className='column-link__icon' fixedWidth />
{antenna.get('title')}
</div>
<div className='account__relationship'>
{button}
</div>
</div>
</div>
);
}
}
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(Antenna));

View file

@ -1,84 +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 { setupAntennaAdder, resetAntennaAdder, setupExcludeAntennaAdder } from '../../actions/antennas';
import NewAntennaForm from '../antennas/components/new_antenna_form';
import Account from '../list_adder/components/account';
import Antenna from './components/antenna';
// hack
const getOrderedAntennas = createSelector([state => state.get('antennas')], antennas => {
if (!antennas) {
return antennas;
}
return antennas.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
antennaIds: getOrderedAntennas(state).map(antenna=>antenna.get('id')),
});
const mapDispatchToProps = dispatch => ({
onInitialize: accountId => dispatch(setupAntennaAdder(accountId)),
onExcludeInitialize: accountId => dispatch(setupExcludeAntennaAdder(accountId)),
onReset: () => dispatch(resetAntennaAdder()),
});
class AntennaAdder extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
isExclude: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onInitialize: PropTypes.func.isRequired,
onExcludeInitialize: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
antennaIds: ImmutablePropTypes.list.isRequired,
};
componentDidMount () {
const { isExclude, onInitialize, onExcludeInitialize, accountId } = this.props;
if (isExclude) {
onExcludeInitialize(accountId);
} else {
onInitialize(accountId);
}
}
componentWillUnmount () {
const { onReset } = this.props;
onReset();
}
render () {
const { accountId, antennaIds, isExclude } = this.props;
return (
<div className='modal-root__modal list-adder'>
<div className='list-adder__account'>
<Account accountId={accountId} />
</div>
<NewAntennaForm />
<div className='list-adder__lists'>
{antennaIds.map(antennaId => <Antenna key={antennaId} antennaId={antennaId} isExclude={isExclude} />)}
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaAdder));

View file

@ -0,0 +1,227 @@
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 AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { fetchAntennas } from 'mastodon/actions/antennas';
import { createAntenna } from 'mastodon/actions/antennas_typed';
import {
apiGetAccountAntennas,
apiAddAccountToAntenna,
apiAddExcludeAccountToAntenna,
apiRemoveAccountFromAntenna,
apiRemoveExcludeAccountFromAntenna,
} from 'mastodon/api/antennas';
import type { ApiAntennaJSON } from 'mastodon/api_types/antennas';
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 { getOrderedAntennas } from 'mastodon/selectors/antennas';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
newAntenna: {
id: 'antennas.new_antenna_name',
defaultMessage: 'New antenna name',
},
createAntenna: {
id: 'antennas.create',
defaultMessage: 'Create',
},
close: {
id: 'lightbox.close',
defaultMessage: 'Close',
},
});
const AntennaItem: 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='antennas__item'>
<div className='antennas__item__title'>
<Icon id='antenna-ul' icon={AntennaIcon} />
<span>{title}</span>
</div>
<CheckBox value={id} checked={checked} onChange={handleChange} />
</label>
);
};
const NewAntennaItem: React.FC<{
onCreate: (antenna: ApiAntennaJSON) => 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(createAntenna({ title })).then((result) => {
if (isFulfilled(result)) {
onCreate(result.payload);
setTitle('');
}
return '';
});
}, [setTitle, dispatch, onCreate, title]);
return (
<form className='antennas__item' onSubmit={handleSubmit}>
<label className='antennas__item__title'>
<Icon id='antenna-ul' icon={AntennaIcon} />
<input
type='text'
value={title}
onChange={handleChange}
maxLength={30}
required
placeholder={intl.formatMessage(messages.newAntenna)}
/>
</label>
<Button text={intl.formatMessage(messages.createAntenna)} type='submit' />
</form>
);
};
const AntennaAdder: React.FC<{
accountId: string;
isExclude: boolean;
onClose: () => void;
}> = ({ accountId, isExclude, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const antennas = useAppSelector((state) => getOrderedAntennas(state));
const [antennaIds, setAntennaIds] = useState<string[]>([]);
useEffect(() => {
dispatch(fetchAntennas());
apiGetAccountAntennas(accountId)
.then((data) => {
setAntennaIds(data.map((l) => l.id));
return '';
})
.catch(() => {
// Nothing
});
}, [dispatch, setAntennaIds, accountId]);
const handleToggle = useCallback(
(antennaId: string, checked: boolean) => {
if (checked) {
setAntennaIds((currentAntennaIds) => [antennaId, ...currentAntennaIds]);
const func = isExclude
? apiAddExcludeAccountToAntenna
: apiAddAccountToAntenna;
func(antennaId, accountId).catch(() => {
setAntennaIds((currentAntennaIds) =>
currentAntennaIds.filter((id) => id !== antennaId),
);
});
} else {
setAntennaIds((currentAntennaIds) =>
currentAntennaIds.filter((id) => id !== antennaId),
);
const func = isExclude
? apiRemoveExcludeAccountFromAntenna
: apiRemoveAccountFromAntenna;
func(antennaId, accountId).catch(() => {
setAntennaIds((currentAntennaIds) => [
antennaId,
...currentAntennaIds,
]);
});
}
},
[setAntennaIds, accountId, isExclude],
);
const handleCreate = useCallback(
(antenna: ApiAntennaJSON) => {
setAntennaIds((currentAntennaIds) => [antenna.id, ...currentAntennaIds]);
apiAddAccountToAntenna(antenna.id, accountId).catch(() => {
setAntennaIds((currentAntennaIds) =>
currentAntennaIds.filter((id) => id !== antenna.id),
);
});
},
[setAntennaIds, 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='antennas.add_to_antennas'
defaultMessage='Add {name} to antennas'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
</span>
</div>
<div className='dialog-modal__content'>
<div className='antennas-scrollable'>
<NewAntennaItem onCreate={handleCreate} />
{antennas.map((antenna) => (
<AntennaItem
key={antenna.id}
id={antenna.id}
title={antenna.title}
checked={antennaIds.includes(antenna.id)}
onChange={handleToggle}
/>
))}
</div>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default AntennaAdder;