Merge remote-tracking branch 'parent/main' into upstream-20241126
This commit is contained in:
commit
8a075ba4c6
303 changed files with 7495 additions and 4498 deletions
227
app/javascript/mastodon/features/antenna_adder/index.tsx
Normal file
227
app/javascript/mastodon/features/antenna_adder/index.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue