kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Allow editing filters
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-filters-v2-hhz42m/deployments/2751
rodzic
ebe4f9373b
commit
af314ee55d
|
@ -12,10 +12,18 @@ const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST';
|
|||
const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS';
|
||||
const FILTERS_FETCH_FAIL = 'FILTERS_FETCH_FAIL';
|
||||
|
||||
const FILTER_FETCH_REQUEST = 'FILTER_FETCH_REQUEST';
|
||||
const FILTER_FETCH_SUCCESS = 'FILTER_FETCH_SUCCESS';
|
||||
const FILTER_FETCH_FAIL = 'FILTER_FETCH_FAIL';
|
||||
|
||||
const FILTERS_CREATE_REQUEST = 'FILTERS_CREATE_REQUEST';
|
||||
const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS';
|
||||
const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL';
|
||||
|
||||
const FILTERS_UPDATE_REQUEST = 'FILTERS_UPDATE_REQUEST';
|
||||
const FILTERS_UPDATE_SUCCESS = 'FILTERS_UPDATE_SUCCESS';
|
||||
const FILTERS_UPDATE_FAIL = 'FILTERS_UPDATE_FAIL';
|
||||
|
||||
const FILTERS_DELETE_REQUEST = 'FILTERS_DELETE_REQUEST';
|
||||
const FILTERS_DELETE_SUCCESS = 'FILTERS_DELETE_SUCCESS';
|
||||
const FILTERS_DELETE_FAIL = 'FILTERS_DELETE_FAIL';
|
||||
|
@ -34,7 +42,7 @@ const fetchFiltersV1 = () =>
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
return api(getState)
|
||||
.get('/api/v1/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
@ -56,7 +64,7 @@ const fetchFiltersV2 = () =>
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
api(getState)
|
||||
return api(getState)
|
||||
.get('/api/v2/filters')
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTERS_FETCH_SUCCESS,
|
||||
|
@ -84,6 +92,61 @@ const fetchFilters = (fromFiltersPage = false) =>
|
|||
if (features.filters) return dispatch(fetchFiltersV1());
|
||||
};
|
||||
|
||||
const fetchFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v1/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilterV2 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({
|
||||
type: FILTER_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
return api(getState)
|
||||
.get(`/api/v2/filters/${id}`)
|
||||
.then(({ data }) => dispatch({
|
||||
type: FILTER_FETCH_SUCCESS,
|
||||
filter: data,
|
||||
skipLoading: true,
|
||||
}))
|
||||
.catch(err => dispatch({
|
||||
type: FILTER_FETCH_FAIL,
|
||||
err,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchFilter = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(fetchFilterV2(id));
|
||||
|
||||
if (features.filters) return dispatch(fetchFilterV1(id));
|
||||
};
|
||||
|
||||
const createFilterV1 = (title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_CREATE_REQUEST });
|
||||
|
@ -129,6 +192,51 @@ const createFilter = (title: string, expires_at: string, context: Array<string>,
|
|||
return dispatch(createFilterV1(title, expires_at, context, hide, keywords));
|
||||
};
|
||||
|
||||
const updateFilterV1 = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v1/filters/${id}`, {
|
||||
phrase: keywords[0].keyword,
|
||||
context,
|
||||
irreversible: hide,
|
||||
whole_word: keywords[0].whole_word,
|
||||
expires_at,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilterV2 = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_UPDATE_REQUEST });
|
||||
return api(getState).patch(`/api/v2/filters/${id}`, {
|
||||
title,
|
||||
context,
|
||||
filter_action: hide ? 'hide' : 'warn',
|
||||
expires_at,
|
||||
keywords_attributes,
|
||||
}).then(response => {
|
||||
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
|
||||
toast.success(messages.added);
|
||||
}).catch(error => {
|
||||
dispatch({ type: FILTERS_UPDATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const updateFilter = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_at, context, hide, keywords));
|
||||
|
||||
return dispatch(updateFilterV1(id, title, expires_at, context, hide, keywords));
|
||||
};
|
||||
|
||||
const deleteFilterV1 = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: FILTERS_DELETE_REQUEST });
|
||||
|
@ -166,13 +274,21 @@ export {
|
|||
FILTERS_FETCH_REQUEST,
|
||||
FILTERS_FETCH_SUCCESS,
|
||||
FILTERS_FETCH_FAIL,
|
||||
FILTER_FETCH_REQUEST,
|
||||
FILTER_FETCH_SUCCESS,
|
||||
FILTER_FETCH_FAIL,
|
||||
FILTERS_CREATE_REQUEST,
|
||||
FILTERS_CREATE_SUCCESS,
|
||||
FILTERS_CREATE_FAIL,
|
||||
FILTERS_UPDATE_REQUEST,
|
||||
FILTERS_UPDATE_SUCCESS,
|
||||
FILTERS_UPDATE_FAIL,
|
||||
FILTERS_DELETE_REQUEST,
|
||||
FILTERS_DELETE_SUCCESS,
|
||||
FILTERS_DELETE_FAIL,
|
||||
fetchFilters,
|
||||
fetchFilter,
|
||||
createFilter,
|
||||
updateFilter,
|
||||
deleteFilter,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { createFilter, fetchFilter, updateFilter } from 'soapbox/actions/filters';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import { Button, Column, Form, FormActions, FormGroup, HStack, Input, Stack, Streamfield, Text, Toggle } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { normalizeFilter } from 'soapbox/normalizers';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
|
||||
interface IFilterField {
|
||||
keyword: string
|
||||
whole_word: boolean
|
||||
}
|
||||
|
||||
interface IEditFilter {
|
||||
params: { id?: string }
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
|
||||
subheading_edit: { id: 'column.filters.subheading_edit', defaultMessage: 'Edit Filter' },
|
||||
title: { id: 'column.filters.title', defaultMessage: 'Title' },
|
||||
keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
|
||||
keywords: { id: 'column.filters.keywords', defaultMessage: 'Keywords or phrases' },
|
||||
expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
|
||||
expires_hint: { id: 'column.filters.expires_hint', defaultMessage: 'Expiration dates are not currently supported' },
|
||||
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
|
||||
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
|
||||
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
|
||||
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
|
||||
accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' },
|
||||
drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
|
||||
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
|
||||
hide_header: { id: 'column.filters.hide_header', defaultMessage: 'Hide completely' },
|
||||
hide_hint: { id: 'column.filters.hide_hint', defaultMessage: 'Completely hide the filtered content, instead of showing a warning' },
|
||||
whole_word_header: { id: 'column.filters.whole_word_header', defaultMessage: 'Whole word' },
|
||||
whole_word_hint: { id: 'column.filters.whole_word_hint', defaultMessage: 'When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word' },
|
||||
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
|
||||
edit: { id: 'column.filters.edit', defaultMessage: 'Edit Filter' },
|
||||
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
|
||||
});
|
||||
|
||||
// const expirations = {
|
||||
// null: 'Never',
|
||||
// // 1800: '30 minutes',
|
||||
// // 3600: '1 hour',
|
||||
// // 21600: '6 hour',
|
||||
// // 43200: '12 hours',
|
||||
// // 86400 : '1 day',
|
||||
// // 604800: '1 week',
|
||||
// };
|
||||
|
||||
const FilterField: StreamfieldComponent<IFilterField> = ({ value, onChange }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleChange = (key: string): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
e => onChange({ ...value, [key]: e.currentTarget[e.currentTarget.type === 'checkbox' ? 'checked' : 'value'] });
|
||||
|
||||
return (
|
||||
<HStack space={2} grow>
|
||||
<Input
|
||||
type='text'
|
||||
outerClassName='w-2/5 grow'
|
||||
value={value.keyword}
|
||||
onChange={handleChange('keyword')}
|
||||
placeholder={intl.formatMessage(messages.keyword)}
|
||||
/>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Toggle
|
||||
checked={value.whole_word}
|
||||
onChange={handleChange('whole_word')}
|
||||
icons={false}
|
||||
/>
|
||||
|
||||
<Text tag='span' theme='muted'>
|
||||
<FormattedMessage id='column.filters.whole_word' defaultMessage='Whole word' />
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
const EditFilter: React.FC<IEditFilter> = ({ params }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [expiresAt] = useState('');
|
||||
const [homeTimeline, setHomeTimeline] = useState(true);
|
||||
const [publicTimeline, setPublicTimeline] = useState(false);
|
||||
const [notifications, setNotifications] = useState(false);
|
||||
const [conversations, setConversations] = useState(false);
|
||||
const [accounts, setAccounts] = useState(false);
|
||||
const [hide, setHide] = useState(false);
|
||||
const [keywords, setKeywords] = useState<{ id?: string, keyword: string, whole_word: boolean }[]>([{ keyword: '', whole_word: false }]);
|
||||
|
||||
// const handleSelectChange = e => {
|
||||
// this.setState({ [e.target.name]: e.target.value });
|
||||
// };
|
||||
|
||||
const handleAddNew: React.FormEventHandler = e => {
|
||||
e.preventDefault();
|
||||
const context: Array<string> = [];
|
||||
|
||||
if (homeTimeline) {
|
||||
context.push('home');
|
||||
}
|
||||
if (publicTimeline) {
|
||||
context.push('public');
|
||||
}
|
||||
if (notifications) {
|
||||
context.push('notifications');
|
||||
}
|
||||
if (conversations) {
|
||||
context.push('thread');
|
||||
}
|
||||
if (accounts) {
|
||||
context.push('account');
|
||||
}
|
||||
|
||||
dispatch(params.id
|
||||
? updateFilter(params.id, title, expiresAt, context, hide, keywords)
|
||||
: createFilter(title, expiresAt, context, hide, keywords)).then(() => {
|
||||
history.push('/filters');
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.create_error));
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeKeyword = (keywords: { keyword: string, whole_word: boolean }[]) => setKeywords(keywords);
|
||||
|
||||
const handleAddKeyword = () => setKeywords(keywords => [...keywords, { keyword: '', whole_word: false }]);
|
||||
|
||||
const handleRemoveKeyword = (i: number) => setKeywords(keywords => keywords.filter((_, index) => index !== i));
|
||||
|
||||
useEffect(() => {
|
||||
if (params.id) {
|
||||
setLoading(true);
|
||||
dispatch(fetchFilter(params.id))?.then((res: any) => {
|
||||
if (res.filter) {
|
||||
const filter = normalizeFilter(res.filter);
|
||||
|
||||
setTitle(filter.title);
|
||||
setHomeTimeline(filter.context.includes('home'));
|
||||
setPublicTimeline(filter.context.includes('public'));
|
||||
setNotifications(filter.context.includes('notifications'));
|
||||
setConversations(filter.context.includes('thread'));
|
||||
setAccounts(filter.context.includes('account'));
|
||||
setHide(filter.filter_action === 'hide');
|
||||
setKeywords(filter.keywords.toJS());
|
||||
} else {
|
||||
setNotFound(true);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [params.id]);
|
||||
|
||||
if (notFound) return <MissingIndicator />;
|
||||
|
||||
return (
|
||||
<Column className='filter-settings-panel' label={intl.formatMessage(messages.subheading_add_new)}>
|
||||
<Form onSubmit={handleAddNew}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.title)}>
|
||||
<Input
|
||||
required
|
||||
type='text'
|
||||
name='title'
|
||||
value={title}
|
||||
onChange={({ target }) => setTitle(target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
|
||||
<SelectDropdown
|
||||
items={expirations}
|
||||
defaultValue={expirations.never}
|
||||
onChange={this.handleSelectChange}
|
||||
/>
|
||||
</FormGroup> */}
|
||||
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium'>
|
||||
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||
</Text>
|
||||
<Text size='xs' theme='muted'>
|
||||
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.home_timeline)}>
|
||||
<Toggle
|
||||
name='home_timeline'
|
||||
checked={homeTimeline}
|
||||
onChange={({ target }) => setHomeTimeline(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.public_timeline)}>
|
||||
<Toggle
|
||||
name='public_timeline'
|
||||
checked={publicTimeline}
|
||||
onChange={({ target }) => setPublicTimeline(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.notifications)}>
|
||||
<Toggle
|
||||
name='notifications'
|
||||
checked={notifications}
|
||||
onChange={({ target }) => setNotifications(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.conversations)}>
|
||||
<Toggle
|
||||
name='conversations'
|
||||
checked={conversations}
|
||||
onChange={({ target }) => setConversations(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
{features.filtersV2 && (
|
||||
<ListItem label={intl.formatMessage(messages.accounts)}>
|
||||
<Toggle
|
||||
name='accounts'
|
||||
checked={accounts}
|
||||
onChange={({ target }) => setAccounts(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
label={intl.formatMessage(features.filtersV2 ? messages.hide_header : messages.drop_header)}
|
||||
hint={intl.formatMessage(features.filtersV2 ? messages.hide_hint : messages.drop_hint)}
|
||||
>
|
||||
<Toggle
|
||||
name='hide'
|
||||
checked={hide}
|
||||
onChange={({ target }) => setHide(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.keywords)}
|
||||
component={FilterField}
|
||||
values={keywords}
|
||||
onChange={handleChangeKeyword}
|
||||
onAddItem={handleAddKeyword}
|
||||
onRemoveItem={handleRemoveKeyword}
|
||||
minItems={1}
|
||||
maxItems={features.filtersV2 ? Infinity : 1}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' theme='primary' disabled={loading}>
|
||||
{intl.formatMessage(params.id ? messages.edit : messages.add_new)}
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditFilter;
|
|
@ -1,20 +1,13 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { fetchFilters, deleteFilter } from 'soapbox/actions/filters';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, HStack, IconButton, Input, Stack, Streamfield, Text, Toggle } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { Button, CardTitle, Column, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
|
||||
interface IFilterField {
|
||||
keyword: string
|
||||
whole_word: boolean
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
||||
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
|
||||
|
@ -59,83 +52,14 @@ const contexts = {
|
|||
// // 604800: '1 week',
|
||||
// };
|
||||
|
||||
const FilterField: StreamfieldComponent<IFilterField> = ({ value, onChange }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleChange = (key: string): React.ChangeEventHandler<HTMLInputElement> =>
|
||||
e => onChange({ ...value, [key]: e.currentTarget[e.currentTarget.type === 'checkbox' ? 'checked' : 'value'] });
|
||||
|
||||
return (
|
||||
<HStack space={2} grow>
|
||||
<Input
|
||||
type='text'
|
||||
outerClassName='w-2/5 grow'
|
||||
value={value.keyword}
|
||||
onChange={handleChange('keyword')}
|
||||
placeholder={intl.formatMessage(messages.keyword)}
|
||||
/>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Toggle
|
||||
checked={value.whole_word}
|
||||
onChange={handleChange('whole_word')}
|
||||
icons={false}
|
||||
/>
|
||||
|
||||
<Text tag='span' theme='muted'>
|
||||
<FormattedMessage id='column.filters.whole_word' defaultMessage='Whole word' />
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
const Filters = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const history = useHistory();
|
||||
|
||||
const filters = useAppSelector((state) => state.filters);
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [expiresAt] = useState('');
|
||||
const [homeTimeline, setHomeTimeline] = useState(true);
|
||||
const [publicTimeline, setPublicTimeline] = useState(false);
|
||||
const [notifications, setNotifications] = useState(false);
|
||||
const [conversations, setConversations] = useState(false);
|
||||
const [accounts, setAccounts] = useState(false);
|
||||
const [hide, setHide] = useState(false);
|
||||
const [keywords, setKeywords] = useState<{ keyword: string, whole_word: boolean }[]>([{ keyword: '', whole_word: false }]);
|
||||
|
||||
// const handleSelectChange = e => {
|
||||
// this.setState({ [e.target.name]: e.target.value });
|
||||
// };
|
||||
|
||||
const handleAddNew: React.FormEventHandler = e => {
|
||||
e.preventDefault();
|
||||
const context: Array<string> = [];
|
||||
|
||||
if (homeTimeline) {
|
||||
context.push('home');
|
||||
}
|
||||
if (publicTimeline) {
|
||||
context.push('public');
|
||||
}
|
||||
if (notifications) {
|
||||
context.push('notifications');
|
||||
}
|
||||
if (conversations) {
|
||||
context.push('thread');
|
||||
}
|
||||
if (accounts) {
|
||||
context.push('account');
|
||||
}
|
||||
|
||||
dispatch(createFilter(title, expiresAt, context, hide, keywords)).then(() => {
|
||||
return dispatch(fetchFilters(true));
|
||||
}).catch(error => {
|
||||
toast.error(intl.formatMessage(messages.create_error));
|
||||
});
|
||||
};
|
||||
const handleFilterEdit = (id: string) => () => history.push(`/filters/${id}`);
|
||||
|
||||
const handleFilterDelete = (id: string) => () => {
|
||||
dispatch(deleteFilter(id)).then(() => {
|
||||
|
@ -145,12 +69,6 @@ const Filters = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleChangeKeyword = (keywords: { keyword: string, whole_word: boolean }[]) => setKeywords(keywords);
|
||||
|
||||
const handleAddKeyword = () => setKeywords(keywords => [...keywords, { keyword: '', whole_word: false }]);
|
||||
|
||||
const handleRemoveKeyword = (i: number) => setKeywords(keywords => keywords.filter((_, index) => index !== i));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchFilters(true));
|
||||
}, []);
|
||||
|
@ -159,118 +77,16 @@ const Filters = () => {
|
|||
|
||||
return (
|
||||
<Column className='filter-settings-panel' label={intl.formatMessage(messages.heading)}>
|
||||
<CardHeader>
|
||||
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
|
||||
</CardHeader>
|
||||
<Form onSubmit={handleAddNew}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.title)}>
|
||||
<Input
|
||||
required
|
||||
type='text'
|
||||
name='title'
|
||||
value={title}
|
||||
onChange={({ target }) => setTitle(target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
|
||||
<SelectDropdown
|
||||
items={expirations}
|
||||
defaultValue={expirations.never}
|
||||
onChange={this.handleSelectChange}
|
||||
/>
|
||||
</FormGroup> */}
|
||||
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium'>
|
||||
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||
</Text>
|
||||
<Text size='xs' theme='muted'>
|
||||
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.home_timeline)}>
|
||||
<Toggle
|
||||
name='home_timeline'
|
||||
checked={homeTimeline}
|
||||
onChange={({ target }) => setHomeTimeline(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.public_timeline)}>
|
||||
<Toggle
|
||||
name='public_timeline'
|
||||
checked={publicTimeline}
|
||||
onChange={({ target }) => setPublicTimeline(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.notifications)}>
|
||||
<Toggle
|
||||
name='notifications'
|
||||
checked={notifications}
|
||||
onChange={({ target }) => setNotifications(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.conversations)}>
|
||||
<Toggle
|
||||
name='conversations'
|
||||
checked={conversations}
|
||||
onChange={({ target }) => setConversations(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
{features.filtersV2 && (
|
||||
<ListItem label={intl.formatMessage(messages.accounts)}>
|
||||
<Toggle
|
||||
name='accounts'
|
||||
checked={accounts}
|
||||
onChange={({ target }) => setAccounts(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
label={intl.formatMessage(features.filtersV2 ? messages.hide_header : messages.drop_header)}
|
||||
hint={intl.formatMessage(features.filtersV2 ? messages.hide_hint : messages.drop_hint)}
|
||||
>
|
||||
<Toggle
|
||||
name='hide'
|
||||
checked={hide}
|
||||
onChange={({ target }) => setHide(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
{/* <ListItem
|
||||
label={intl.formatMessage(messages.whole_word_header)}
|
||||
hint={intl.formatMessage(messages.whole_word_hint)}
|
||||
>
|
||||
<Toggle
|
||||
name='whole_word'
|
||||
checked={wholeWord}
|
||||
onChange={({ target }) => setWholeWord(target.checked)}
|
||||
/>
|
||||
</ListItem> */}
|
||||
</List>
|
||||
|
||||
<Streamfield
|
||||
label={intl.formatMessage(messages.keywords)}
|
||||
component={FilterField}
|
||||
values={keywords}
|
||||
onChange={handleChangeKeyword}
|
||||
onAddItem={handleAddKeyword}
|
||||
onRemoveItem={handleRemoveKeyword}
|
||||
minItems={1}
|
||||
maxItems={features.filtersV2 ? Infinity : 1}
|
||||
/>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
|
||||
<CardHeader>
|
||||
<HStack className='mb-4' space={2} justifyContent='between'>
|
||||
<CardTitle title={intl.formatMessage(messages.subheading_filters)} />
|
||||
</CardHeader>
|
||||
<Button
|
||||
to='/filters/new'
|
||||
theme='primary'
|
||||
size='sm'
|
||||
>
|
||||
<FormattedMessage id='filters.create_filter' defaultMessage='Create filter' />
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
|
@ -278,8 +94,8 @@ const Filters = () => {
|
|||
itemClassName='pb-4 last:pb-0'
|
||||
>
|
||||
{filters.map((filter, i) => (
|
||||
<HStack space={1} justifyContent='between'>
|
||||
<Stack space={1}>
|
||||
<HStack space={1}>
|
||||
<Stack className='grow' space={1}>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' />
|
||||
{' '}
|
||||
|
@ -303,6 +119,12 @@ const Filters = () => {
|
|||
)} */}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<IconButton
|
||||
iconClassName='h-5 w-5 text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
src={require('@tabler/icons/pencil.svg')}
|
||||
onClick={handleFilterEdit(filter.id)}
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
/>
|
||||
<IconButton
|
||||
iconClassName='h-5 w-5 text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
src={require('@tabler/icons/trash.svg')}
|
||||
|
|
|
@ -66,6 +66,7 @@ import {
|
|||
DomainBlocks,
|
||||
Mutes,
|
||||
Filters,
|
||||
EditFilter,
|
||||
PinnedStatuses,
|
||||
Search,
|
||||
ListTimeline,
|
||||
|
@ -266,6 +267,8 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<WrappedRoute path='/blocks' page={DefaultPage} component={Blocks} content={children} />
|
||||
{features.federating && <WrappedRoute path='/domain_blocks' page={DefaultPage} component={DomainBlocks} content={children} />}
|
||||
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
|
||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/new' page={DefaultPage} component={EditFilter} content={children} />}
|
||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters/:id' page={DefaultPage} component={EditFilter} content={children} />}
|
||||
{(features.filters || features.filtersV2) && <WrappedRoute path='/filters' page={DefaultPage} component={Filters} content={children} />}
|
||||
<WrappedRoute path='/@:username' publicRoute exact component={AccountTimeline} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} />
|
||||
|
|
|
@ -102,6 +102,10 @@ export function Filters() {
|
|||
return import(/* webpackChunkName: "features/filters" */'../../filters');
|
||||
}
|
||||
|
||||
export function EditFilter() {
|
||||
return import(/* webpackChunkName: "features/filters" */'../../filters/edit-filter');
|
||||
}
|
||||
|
||||
export function ReportModal() {
|
||||
return import(/* webpackChunkName: "modals/report-modal/report-modal" */'../components/modals/report-modal/report-modal');
|
||||
}
|
||||
|
|
|
@ -452,7 +452,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
|
||||
/**
|
||||
* Can edit and manage timeline filters (aka "muted words").
|
||||
* @see {@link https://docs.joinmastodon.org/methods/accounts/filters/}
|
||||
* @see {@link https://docs.joinmastodon.org/methods/filters/}
|
||||
*/
|
||||
filtersV2: v.software === MASTODON && gte(v.compatVersion, '3.6.0'),
|
||||
|
||||
|
@ -788,7 +788,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
|
||||
/**
|
||||
* Can display suggested accounts.
|
||||
* @see {@link https://docs.joinmastodon.org/methods/accounts/suggestions/}
|
||||
* @see {@link https://docs.joinmastodon.org/methods/suggestions/}
|
||||
*/
|
||||
suggestions: any([
|
||||
v.software === MASTODON && gte(v.compatVersion, '2.4.3'),
|
||||
|
|
Ładowanie…
Reference in New Issue