Wip: アンテナ編集画面 アカウント・ドメイン
This commit is contained in:
parent
8a075ba4c6
commit
041b05b15f
8 changed files with 481 additions and 78 deletions
|
@ -28,6 +28,16 @@ export const apiGetDomains = (antennaId: string) =>
|
||||||
limit: 0,
|
limit: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiAddDomain = (antennaId: string, domain: string) =>
|
||||||
|
apiRequestPost(`v1/antennas/${antennaId}/domains`, {
|
||||||
|
domains: [domain],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiRemoveDomain = (antennaId: string, domain: string) =>
|
||||||
|
apiRequestDelete(`v1/antennas/${antennaId}/domains`, {
|
||||||
|
domains: [domain],
|
||||||
|
});
|
||||||
|
|
||||||
export const apiGetExcludeDomains = (antennaId: string) =>
|
export const apiGetExcludeDomains = (antennaId: string) =>
|
||||||
apiRequestGet<string[]>(`v1/antennas/${antennaId}/exclude_domains`, {
|
apiRequestGet<string[]>(`v1/antennas/${antennaId}/exclude_domains`, {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
|
|
|
@ -10,17 +10,7 @@ export interface ApiAntennaJSON {
|
||||||
insert_feeds: boolean;
|
insert_feeds: boolean;
|
||||||
with_media_only: boolean;
|
with_media_only: boolean;
|
||||||
ignore_reblog: boolean;
|
ignore_reblog: boolean;
|
||||||
accounts_count: number;
|
|
||||||
domains_count: number;
|
|
||||||
tags_count: number;
|
|
||||||
keywords_count: number;
|
|
||||||
list: ApiListJSON | null;
|
list: ApiListJSON | null;
|
||||||
domains: string[] | undefined;
|
|
||||||
exclude_domains: string[] | undefined;
|
|
||||||
keywords: string[] | undefined;
|
|
||||||
exclude_keywords: string[] | undefined;
|
|
||||||
tags: string[] | undefined;
|
|
||||||
exclude_tags: string[] | undefined;
|
|
||||||
|
|
||||||
list_id: string | undefined;
|
list_id: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
417
app/javascript/mastodon/features/antenna_setting/index.tsx
Normal file
417
app/javascript/mastodon/features/antenna_setting/index.tsx
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
|
import DomainIcon from '@/material-icons/400-24px/dns.svg?react';
|
||||||
|
//import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
|
//import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
|
//import KeywordIcon from '@/material-icons/400-24px/title.svg?react';
|
||||||
|
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||||
|
import {
|
||||||
|
fetchAntenna,
|
||||||
|
fetchAntennaAccounts,
|
||||||
|
fetchAntennaDomains,
|
||||||
|
fetchAntennaExcludeAccounts,
|
||||||
|
fetchAntennaKeywords,
|
||||||
|
fetchAntennaTags,
|
||||||
|
} from 'mastodon/actions/antennas';
|
||||||
|
import {
|
||||||
|
apiAddDomain,
|
||||||
|
apiGetAccounts,
|
||||||
|
apiRemoveDomain,
|
||||||
|
} from 'mastodon/api/antennas';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import Column from 'mastodon/components/column';
|
||||||
|
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||||
|
import type { IconProp , Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
deleteMessage: {
|
||||||
|
id: 'confirmations.delete_antenna.message',
|
||||||
|
defaultMessage: 'Are you sure you want to permanently delete this antenna?',
|
||||||
|
},
|
||||||
|
deleteConfirm: {
|
||||||
|
id: 'confirmations.delete_antenna.confirm',
|
||||||
|
defaultMessage: 'Delete',
|
||||||
|
},
|
||||||
|
editAccounts: {
|
||||||
|
id: 'antennas.edit_accounts',
|
||||||
|
defaultMessage: 'Edit accounts',
|
||||||
|
},
|
||||||
|
noOptions: {
|
||||||
|
id: 'antennas.select.no_options_message',
|
||||||
|
defaultMessage: 'Empty lists',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
id: 'antennas.select.placeholder',
|
||||||
|
defaultMessage: 'Select list',
|
||||||
|
},
|
||||||
|
addDomainLabel: {
|
||||||
|
id: 'antennas.add_domain_placeholder',
|
||||||
|
defaultMessage: 'New domain',
|
||||||
|
},
|
||||||
|
addKeywordLabel: {
|
||||||
|
id: 'antennas.add_keyword_placeholder',
|
||||||
|
defaultMessage: 'New keyword',
|
||||||
|
},
|
||||||
|
addTagLabel: {
|
||||||
|
id: 'antennas.add_tag_placeholder',
|
||||||
|
defaultMessage: 'New tag',
|
||||||
|
},
|
||||||
|
addDomainTitle: { id: 'antennas.add_domain', defaultMessage: 'Add domain' },
|
||||||
|
addKeywordTitle: {
|
||||||
|
id: 'antennas.add_keyword',
|
||||||
|
defaultMessage: 'Add keyword',
|
||||||
|
},
|
||||||
|
addTagTitle: { id: 'antennas.add_tag', defaultMessage: 'Add tag' },
|
||||||
|
accounts: { id: 'antennas.accounts', defaultMessage: '{count} accounts' },
|
||||||
|
domains: { id: 'antennas.domains', defaultMessage: '{count} domains' },
|
||||||
|
tags: { id: 'antennas.tags', defaultMessage: '{count} tags' },
|
||||||
|
keywords: { id: 'antennas.keywords', defaultMessage: '{count} keywords' },
|
||||||
|
setHome: { id: 'antennas.select.set_home', defaultMessage: 'Set home' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const TextListItem: React.FC<{
|
||||||
|
icon: string;
|
||||||
|
iconComponent: IconProp;
|
||||||
|
value: string;
|
||||||
|
onRemove: (value: string) => void;
|
||||||
|
}> = ({ icon, iconComponent, value, onRemove }) => {
|
||||||
|
const handleRemove = useCallback(() => { onRemove(value); }, [onRemove, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='setting-text-list-item'>
|
||||||
|
<Icon id={icon} icon={iconComponent} />
|
||||||
|
<span className='label'>{value}</span>
|
||||||
|
<IconButton
|
||||||
|
title='Delete'
|
||||||
|
icon='trash'
|
||||||
|
iconComponent={DeleteIcon}
|
||||||
|
onClick={handleRemove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextList: React.FC<{
|
||||||
|
values: string[];
|
||||||
|
disabled?: boolean;
|
||||||
|
icon: string;
|
||||||
|
iconComponent: IconProp;
|
||||||
|
label: string;
|
||||||
|
title: string;
|
||||||
|
onAdd: (value: string) => void;
|
||||||
|
onRemove: (value: string) => void;
|
||||||
|
}> = ({
|
||||||
|
values,
|
||||||
|
disabled,
|
||||||
|
icon,
|
||||||
|
iconComponent,
|
||||||
|
label,
|
||||||
|
title,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
const handleValueChange = useCallback(
|
||||||
|
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setValue(value);
|
||||||
|
},
|
||||||
|
[setValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAdd = useCallback(() => {
|
||||||
|
onAdd(value);
|
||||||
|
setValue('');
|
||||||
|
}, [onAdd, value]);
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(removeValue: string) => { onRemove(removeValue); },
|
||||||
|
[onRemove],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = handleAdd;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='setting-text-list'>
|
||||||
|
{values.map((val) => (
|
||||||
|
<TextListItem
|
||||||
|
key={val}
|
||||||
|
value={val}
|
||||||
|
icon={icon}
|
||||||
|
iconComponent={iconComponent}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<form className='add-text-form' onSubmit={handleSubmit}>
|
||||||
|
<label>
|
||||||
|
<span style={{ display: 'none' }}>{label}</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className='setting-text'
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleValueChange}
|
||||||
|
placeholder={label}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={disabled || !value}
|
||||||
|
text={title}
|
||||||
|
onClick={handleAdd}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RadioPanel: React.FC<{
|
||||||
|
items: { title: string; value: string }[];
|
||||||
|
valueLengths: number[];
|
||||||
|
alertMessage: React.ReactElement;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}> = ({ items, valueLengths, alertMessage, onChange }) => {
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (valueLengths.length >= 2) {
|
||||||
|
setError(valueLengths.slice(1).some((v) => v > 0));
|
||||||
|
} else {
|
||||||
|
setError(false);
|
||||||
|
}
|
||||||
|
}, [valueLengths]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (items.length > 0 && !items.some((i) => i.value === value)) {
|
||||||
|
for (let i = 0; i < valueLengths.length; i++) {
|
||||||
|
const length = valueLengths[i] ?? 0;
|
||||||
|
const item = items[i] ?? { value: '' };
|
||||||
|
if (length > 0) {
|
||||||
|
setValue(item.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setValue(items[0]?.value ?? '');
|
||||||
|
}
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
const selected = currentTarget.getAttribute('data-value') ?? '';
|
||||||
|
if (value !== selected) {
|
||||||
|
onChange(selected);
|
||||||
|
setValue(selected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='setting-radio-panel'>
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
className={classNames('setting-radio-panel__item', {
|
||||||
|
'setting-radio-panel__item__active': value === item.value,
|
||||||
|
})}
|
||||||
|
key={item.value}
|
||||||
|
onClick={handleChange}
|
||||||
|
data-value={item.value}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className='alert'>{alertMessage}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MembersLink: React.FC<{
|
||||||
|
id: string;
|
||||||
|
onCountFetched: (count: number) => void;
|
||||||
|
}> = ({ id, onCountFetched }) => {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
const [avatars, setAvatars] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void apiGetAccounts(id)
|
||||||
|
.then((data) => {
|
||||||
|
setCount(data.length);
|
||||||
|
onCountFetched(data.length);
|
||||||
|
setAvatars(data.slice(0, 3).map((a) => a.avatar));
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Nothing
|
||||||
|
});
|
||||||
|
}, [id, setCount, setAvatars]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/antennas/${id}/members`} className='app-form__link'>
|
||||||
|
<div className='app-form__link__text'>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='antennas.antenna_accounts'
|
||||||
|
defaultMessage='Antenna accounts'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='antennas.antenna_accounts_count'
|
||||||
|
defaultMessage='{count, plural, one {# member} other {# accounts}}'
|
||||||
|
values={{ count }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='avatar-pile'>
|
||||||
|
{avatars.map((url) => (
|
||||||
|
<img key={url} src={url} alt='' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AntennaSetting: React.FC<{
|
||||||
|
multiColumn?: boolean;
|
||||||
|
}> = ({ multiColumn }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { id } = useParams<{ id?: string }>();
|
||||||
|
const intl = useIntl();
|
||||||
|
//const history = useHistory();
|
||||||
|
|
||||||
|
const antenna = useAppSelector((state) =>
|
||||||
|
id ? state.antennas.get(id) : undefined,
|
||||||
|
);
|
||||||
|
const domains = useAppSelector((state) =>
|
||||||
|
id ? state.antennas.getIn(['domains', id]) : undefined,
|
||||||
|
) as string[] | undefined;
|
||||||
|
const [domainList, setDomainList] = useState([] as string[]);
|
||||||
|
const [accountsCount, setAccountsCount] = useState(0);
|
||||||
|
const [rangeMode, setRangeMode] = useState('accounts');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
dispatch(fetchAntenna(id));
|
||||||
|
dispatch(fetchAntennaKeywords(id));
|
||||||
|
dispatch(fetchAntennaDomains(id));
|
||||||
|
dispatch(fetchAntennaTags(id));
|
||||||
|
dispatch(fetchAntennaAccounts(id));
|
||||||
|
dispatch(fetchAntennaExcludeAccounts(id));
|
||||||
|
}
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && antenna) {
|
||||||
|
setDomainList(domains ?? []);
|
||||||
|
}
|
||||||
|
}, [id, antenna, domains, setDomainList]);
|
||||||
|
|
||||||
|
const handleAccountsFetched = useCallback(
|
||||||
|
(count: number) => {
|
||||||
|
setAccountsCount(count);
|
||||||
|
},
|
||||||
|
[setAccountsCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddDomain = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
void apiAddDomain(id, value).then(() => {
|
||||||
|
setDomainList([...domainList, value]);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[id, domainList, setDomainList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveDomain = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
void apiRemoveDomain(id, value).then(() => {
|
||||||
|
setDomainList(domainList.filter((v) => v !== value));
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[id, domainList, setDomainList],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRangeRadioChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
setRangeMode(value);
|
||||||
|
},
|
||||||
|
[id, antenna, setRangeMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!antenna || !id) return <div />;
|
||||||
|
|
||||||
|
const rangeRadioItems = [
|
||||||
|
{
|
||||||
|
value: 'accounts',
|
||||||
|
title: intl.formatMessage(messages.accounts, { count: accountsCount }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'domains',
|
||||||
|
title: intl.formatMessage(messages.domains, { count: domainList.length }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rangeRadioLengths = [0, domainList.length];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} label={antenna.title}>
|
||||||
|
<ColumnHeader
|
||||||
|
title={antenna.title}
|
||||||
|
icon='antenna-ul'
|
||||||
|
iconComponent={AntennaIcon}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='scrollable antenna-setting'>
|
||||||
|
<RadioPanel
|
||||||
|
items={rangeRadioItems}
|
||||||
|
valueLengths={rangeRadioLengths}
|
||||||
|
alertMessage={
|
||||||
|
<div className='alert'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='antennas.warnings.range_radio'
|
||||||
|
defaultMessage='Simultaneous account and domain designation is not recommended.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onChange={handleRangeRadioChange}
|
||||||
|
/>
|
||||||
|
{rangeMode === 'accounts' && (
|
||||||
|
<MembersLink id={id} onCountFetched={handleAccountsFetched} />
|
||||||
|
)}
|
||||||
|
{rangeMode === 'domains' && (
|
||||||
|
<TextList
|
||||||
|
values={domainList}
|
||||||
|
icon='sitemap'
|
||||||
|
iconComponent={DomainIcon}
|
||||||
|
label={intl.formatMessage(messages.addDomainLabel)}
|
||||||
|
title={intl.formatMessage(messages.addDomainTitle)}
|
||||||
|
onAdd={handleAddDomain}
|
||||||
|
onRemove={handleRemoveDomain}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default AntennaSetting;
|
|
@ -12,14 +12,14 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
import { fetchFollowing } from 'mastodon/actions/accounts';
|
import { fetchFollowing } from 'mastodon/actions/accounts';
|
||||||
|
import { fetchAntenna } from 'mastodon/actions/antennas';
|
||||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||||
import { fetchList } from 'mastodon/actions/lists';
|
|
||||||
import { apiRequest } from 'mastodon/api';
|
import { apiRequest } from 'mastodon/api';
|
||||||
import {
|
import {
|
||||||
apiGetAccounts,
|
apiGetAccounts,
|
||||||
apiAddAccountToList,
|
apiAddAccountToAntenna,
|
||||||
apiRemoveAccountFromList,
|
apiRemoveAccountFromAntenna,
|
||||||
} from 'mastodon/api/lists';
|
} from 'mastodon/api/antennas';
|
||||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import { Button } from 'mastodon/components/button';
|
import { Button } from 'mastodon/components/button';
|
||||||
|
@ -36,14 +36,20 @@ import { me } from 'mastodon/initial_state';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
|
heading: {
|
||||||
|
id: 'column.antenna_members',
|
||||||
|
defaultMessage: 'Manage antenna members',
|
||||||
|
},
|
||||||
placeholder: {
|
placeholder: {
|
||||||
id: 'lists.search_placeholder',
|
id: 'antennas.search_placeholder',
|
||||||
defaultMessage: 'Search people you follow',
|
defaultMessage: 'Search people you follow',
|
||||||
},
|
},
|
||||||
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
|
enterSearch: {
|
||||||
add: { id: 'lists.add_member', defaultMessage: 'Add' },
|
id: 'antennas.add_to_antenna',
|
||||||
remove: { id: 'lists.remove_member', defaultMessage: 'Remove' },
|
defaultMessage: 'Add to antenna',
|
||||||
|
},
|
||||||
|
add: { id: 'antennas.add_member', defaultMessage: 'Add' },
|
||||||
|
remove: { id: 'antennas.remove_member', defaultMessage: 'Remove' },
|
||||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -99,22 +105,22 @@ const ColumnSearchHeader: React.FC<{
|
||||||
|
|
||||||
const AccountItem: React.FC<{
|
const AccountItem: React.FC<{
|
||||||
accountId: string;
|
accountId: string;
|
||||||
listId: string;
|
antennaId: string;
|
||||||
partOfList: boolean;
|
partOfAntenna: boolean;
|
||||||
onToggle: (accountId: string) => void;
|
onToggle: (accountId: string) => void;
|
||||||
}> = ({ accountId, listId, partOfList, onToggle }) => {
|
}> = ({ accountId, antennaId, partOfAntenna, onToggle }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (partOfList) {
|
if (partOfAntenna) {
|
||||||
void apiRemoveAccountFromList(listId, accountId);
|
void apiRemoveAccountFromAntenna(antennaId, accountId);
|
||||||
} else {
|
} else {
|
||||||
void apiAddAccountToList(listId, accountId);
|
void apiAddAccountToAntenna(antennaId, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggle(accountId);
|
onToggle(accountId);
|
||||||
}, [accountId, listId, partOfList, onToggle]);
|
}, [accountId, antennaId, partOfAntenna, onToggle]);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -154,7 +160,7 @@ const AccountItem: React.FC<{
|
||||||
<div className='account__relationship'>
|
<div className='account__relationship'>
|
||||||
<Button
|
<Button
|
||||||
text={intl.formatMessage(
|
text={intl.formatMessage(
|
||||||
partOfList ? messages.remove : messages.add,
|
partOfAntenna ? messages.remove : messages.add,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
|
@ -164,16 +170,13 @@ const AccountItem: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListMembers: React.FC<{
|
const AntennaMembers: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const followingAccountIds = useAppSelector(
|
|
||||||
(state) => state.user_lists.getIn(['following', me, 'items']) as string[],
|
|
||||||
);
|
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||||
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||||
|
@ -183,7 +186,7 @@ const ListMembers: React.FC<{
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
dispatch(fetchList(id));
|
dispatch(fetchAntenna(id));
|
||||||
|
|
||||||
void apiGetAccounts(id)
|
void apiGetAccounts(id)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -211,9 +214,9 @@ const ListMembers: React.FC<{
|
||||||
|
|
||||||
const handleAccountToggle = useCallback(
|
const handleAccountToggle = useCallback(
|
||||||
(accountId: string) => {
|
(accountId: string) => {
|
||||||
const partOfList = accountIds.includes(accountId);
|
const partOfAntenna = accountIds.includes(accountId);
|
||||||
|
|
||||||
if (partOfList) {
|
if (partOfAntenna) {
|
||||||
setAccountIds(accountIds.filter((id) => id !== accountId));
|
setAccountIds(accountIds.filter((id) => id !== accountId));
|
||||||
} else {
|
} else {
|
||||||
setAccountIds([accountId, ...accountIds]);
|
setAccountIds([accountId, ...accountIds]);
|
||||||
|
@ -244,7 +247,6 @@ const ListMembers: React.FC<{
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: false,
|
resolve: false,
|
||||||
following: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -266,7 +268,7 @@ const ListMembers: React.FC<{
|
||||||
let displayedAccountIds: string[];
|
let displayedAccountIds: string[];
|
||||||
|
|
||||||
if (mode === 'add') {
|
if (mode === 'add') {
|
||||||
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
|
displayedAccountIds = searching ? searchAccountIds : accountIds;
|
||||||
} else {
|
} else {
|
||||||
displayedAccountIds = accountIds;
|
displayedAccountIds = accountIds;
|
||||||
}
|
}
|
||||||
|
@ -279,7 +281,7 @@ const ListMembers: React.FC<{
|
||||||
{mode === 'remove' ? (
|
{mode === 'remove' ? (
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
title={intl.formatMessage(messages.heading)}
|
title={intl.formatMessage(messages.heading)}
|
||||||
icon='list-ul'
|
icon='antenna-ul'
|
||||||
iconComponent={AntennaIcon}
|
iconComponent={AntennaIcon}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
showBackButton
|
showBackButton
|
||||||
|
@ -303,7 +305,7 @@ const ListMembers: React.FC<{
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='list_members'
|
scrollKey='antenna_members'
|
||||||
trackScroll={!multiColumn}
|
trackScroll={!multiColumn}
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
|
@ -315,8 +317,8 @@ const ListMembers: React.FC<{
|
||||||
{displayedAccountIds.length > 0 && <div className='spacer' />}
|
{displayedAccountIds.length > 0 && <div className='spacer' />}
|
||||||
|
|
||||||
<div className='column-footer'>
|
<div className='column-footer'>
|
||||||
<Link to={`/lists/${id}`} className='button button--block'>
|
<Link to={`/antennasw/${id}`} className='button button--block'>
|
||||||
<FormattedMessage id='lists.done' defaultMessage='Done' />
|
<FormattedMessage id='antennas.done' defaultMessage='Done' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -327,12 +329,12 @@ const ListMembers: React.FC<{
|
||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='lists.no_members_yet'
|
id='antennas.no_members_yet'
|
||||||
defaultMessage='No members yet.'
|
defaultMessage='No members yet.'
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='lists.find_users_to_add'
|
id='antennas.find_users_to_add'
|
||||||
defaultMessage='Find users to add'
|
defaultMessage='Find users to add'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -341,7 +343,7 @@ const ListMembers: React.FC<{
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='lists.no_results_found'
|
id='antennas.no_results_found'
|
||||||
defaultMessage='No results found.'
|
defaultMessage='No results found.'
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -351,8 +353,8 @@ const ListMembers: React.FC<{
|
||||||
<AccountItem
|
<AccountItem
|
||||||
key={accountId}
|
key={accountId}
|
||||||
accountId={accountId}
|
accountId={accountId}
|
||||||
listId={id}
|
antennaId={id}
|
||||||
partOfList={
|
partOfAntenna={
|
||||||
displayedAccountIds === accountIds ||
|
displayedAccountIds === accountIds ||
|
||||||
accountIds.includes(accountId)
|
accountIds.includes(accountId)
|
||||||
}
|
}
|
||||||
|
@ -370,4 +372,4 @@ const ListMembers: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ListMembers;
|
export default AntennaMembers;
|
||||||
|
|
|
@ -24,7 +24,7 @@ const getOrderedAntennas = createSelector([state => state.get('antennas')], ante
|
||||||
return antennas;
|
return antennas;
|
||||||
}
|
}
|
||||||
|
|
||||||
return antennas.toList().filter(item => !!item && !item.get('insert_feeds')).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(8);
|
return antennas.toList().filter(item => !!item && !item.get('insert_feeds') && item.get('title') !== undefined).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ListPanel = () => {
|
export const ListPanel = () => {
|
||||||
|
|
|
@ -14,17 +14,7 @@ const AntennaFactory = Record<AntennaShape>({
|
||||||
insert_feeds: false,
|
insert_feeds: false,
|
||||||
with_media_only: false,
|
with_media_only: false,
|
||||||
ignore_reblog: false,
|
ignore_reblog: false,
|
||||||
accounts_count: 0,
|
|
||||||
domains_count: 0,
|
|
||||||
tags_count: 0,
|
|
||||||
keywords_count: 0,
|
|
||||||
list: null,
|
list: null,
|
||||||
domains: undefined,
|
|
||||||
keywords: undefined,
|
|
||||||
tags: undefined,
|
|
||||||
exclude_domains: undefined,
|
|
||||||
exclude_keywords: undefined,
|
|
||||||
exclude_tags: undefined,
|
|
||||||
list_id: undefined,
|
list_id: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,32 +11,16 @@ import {
|
||||||
ANTENNA_FETCH_FAIL,
|
ANTENNA_FETCH_FAIL,
|
||||||
ANTENNAS_FETCH_SUCCESS,
|
ANTENNAS_FETCH_SUCCESS,
|
||||||
ANTENNA_DELETE_SUCCESS,
|
ANTENNA_DELETE_SUCCESS,
|
||||||
|
ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS,
|
||||||
|
//ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS,
|
||||||
|
//ANTENNA_EDITOR_FETCH_TAGS_SUCCESS,
|
||||||
} from '../actions/antennas';
|
} from '../actions/antennas';
|
||||||
|
|
||||||
const initialState = ImmutableMap<string, Antenna | null>();
|
const initialState = ImmutableMap<string, Antenna | null>();
|
||||||
type State = typeof initialState;
|
type State = typeof initialState;
|
||||||
|
|
||||||
const normalizeAntenna = (state: State, antenna: ApiAntennaJSON) => {
|
const normalizeAntenna = (state: State, antenna: ApiAntennaJSON) =>
|
||||||
let s = state.set(antenna.id, createAntennaFromJSON(antenna));
|
state.set(antenna.id, createAntennaFromJSON(antenna));
|
||||||
|
|
||||||
const old = state.get(antenna.id);
|
|
||||||
if (old === undefined) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (old) {
|
|
||||||
s = s.setIn([antenna.id, 'domains'], old.get('domains'));
|
|
||||||
s = s.setIn([antenna.id, 'exclude_domains'], old.get('exclude_domains'));
|
|
||||||
s = s.setIn([antenna.id, 'keywords'], old.get('keywords'));
|
|
||||||
s = s.setIn([antenna.id, 'exclude_keywords'], old.get('exclude_keywords'));
|
|
||||||
s = s.setIn([antenna.id, 'tags'], old.get('tags'));
|
|
||||||
s = s.setIn([antenna.id, 'exclude_tags'], old.get('exclude_tags'));
|
|
||||||
s = s.setIn([antenna.id, 'accounts_count'], old.get('accounts_count'));
|
|
||||||
s = s.setIn([antenna.id, 'domains_count'], old.get('domains_count'));
|
|
||||||
s = s.setIn([antenna.id, 'keywords_count'], old.get('keywords_count'));
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAntennas = (state: State, antennas: ApiAntennaJSON[]) => {
|
const normalizeAntennas = (state: State, antennas: ApiAntennaJSON[]) => {
|
||||||
antennas.forEach((antenna) => {
|
antennas.forEach((antenna) => {
|
||||||
|
@ -64,6 +48,16 @@ export const antennasReducer: Reducer<State> = (
|
||||||
case ANTENNA_DELETE_SUCCESS:
|
case ANTENNA_DELETE_SUCCESS:
|
||||||
case ANTENNA_FETCH_FAIL:
|
case ANTENNA_FETCH_FAIL:
|
||||||
return state.set(action.id as string, null);
|
return state.set(action.id as string, null);
|
||||||
|
case ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS:
|
||||||
|
return state
|
||||||
|
.setIn(
|
||||||
|
['domains', action.id],
|
||||||
|
(action.domains as { domains: string[] }).domains,
|
||||||
|
)
|
||||||
|
.setIn(
|
||||||
|
['exclude_domains', action.id],
|
||||||
|
(action.domains as { exclude_domains: string[] }).exclude_domains,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue