Filters expiration, restyle filters list, fix keywords deletion

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-03-05 19:49:40 +01:00
parent af314ee55d
commit 1d4d9c2732
6 changed files with 106 additions and 97 deletions

View File

@ -147,7 +147,7 @@ const fetchFilter = (id: string) =>
if (features.filters) return dispatch(fetchFilterV1(id));
};
const createFilterV1 = (title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
const createFilterV1 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_CREATE_REQUEST });
return api(getState).post('/api/v1/filters', {
@ -155,7 +155,7 @@ const createFilterV1 = (title: string, expires_at: string, context: Array<string
context,
irreversible: hide,
whole_word: keywords[0].whole_word,
expires_at,
expires_in,
}).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
toast.success(messages.added);
@ -164,14 +164,14 @@ const createFilterV1 = (title: string, expires_at: string, context: Array<string
});
};
const createFilterV2 = (title: string, expires_at: string, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
const createFilterV2 = (title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_CREATE_REQUEST });
return api(getState).post('/api/v2/filters', {
title,
context,
filter_action: hide ? 'hide' : 'warn',
expires_at,
expires_in,
keywords_attributes,
}).then(response => {
dispatch({ type: FILTERS_CREATE_SUCCESS, filter: response.data });
@ -181,18 +181,18 @@ const createFilterV2 = (title: string, expires_at: string, context: Array<string
});
};
const createFilter = (title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
const createFilter = (title: string, expires_in: string | null, 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(createFilterV2(title, expires_at, context, hide, keywords));
if (features.filtersV2) return dispatch(createFilterV2(title, expires_in, context, hide, keywords));
return dispatch(createFilterV1(title, expires_at, context, hide, keywords));
return dispatch(createFilterV1(title, expires_in, context, hide, keywords));
};
const updateFilterV1 = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: FILTERS_UPDATE_REQUEST });
return api(getState).patch(`/api/v1/filters/${id}`, {
@ -200,7 +200,7 @@ const updateFilterV1 = (id: string, title: string, expires_at: string, context:
context,
irreversible: hide,
whole_word: keywords[0].whole_word,
expires_at,
expires_in,
}).then(response => {
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
toast.success(messages.added);
@ -209,14 +209,14 @@ const updateFilterV1 = (id: string, title: string, expires_at: string, context:
});
};
const updateFilterV2 = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords_attributes: FilterKeywords) =>
const updateFilterV2 = (id: string, title: string, expires_in: string | null, 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,
expires_in,
keywords_attributes,
}).then(response => {
dispatch({ type: FILTERS_UPDATE_SUCCESS, filter: response.data });
@ -226,15 +226,15 @@ const updateFilterV2 = (id: string, title: string, expires_at: string, context:
});
};
const updateFilter = (id: string, title: string, expires_at: string, context: Array<string>, hide: boolean, keywords: FilterKeywords) =>
const updateFilter = (id: string, title: string, expires_in: string | null, 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));
if (features.filtersV2) return dispatch(updateFilterV2(id, title, expires_in, context, hide, keywords));
return dispatch(updateFilterV1(id, title, expires_at, context, hide, keywords));
return dispatch(updateFilterV1(id, title, expires_in, context, hide, keywords));
};
const deleteFilterV1 = (id: string) =>

View File

@ -70,7 +70,7 @@ const Streamfield: React.FC<IStreamfield> = ({
{(values.length > 0) && (
<Stack>
{values.map((value, i) => (
{values.map((value, i) => value?._destroy ? null : (
<HStack space={2} alignItems='center'>
<Component key={i} onChange={handleChange(i)} value={value} />
{values.length > minItems && onRemoveItem && (

View File

@ -3,7 +3,7 @@ import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, HStack, Spinner, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { Token } from 'soapbox/reducers/security';
@ -59,12 +59,11 @@ const AuthToken: React.FC<IAuthToken> = ({ token, isCurrent }) => {
</Text>
)}
</Stack>
<div className='flex justify-end'>
<HStack justifyContent='end'>
<Button theme={isCurrent ? 'danger' : 'primary'} onClick={handleRevoke}>
{intl.formatMessage(messages.revoke)}
</Button>
</div>
</HStack>
</Stack>
</div>
);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
@ -10,11 +10,15 @@ import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import { normalizeFilter } from 'soapbox/normalizers';
import toast from 'soapbox/toast';
import { SelectDropdown } from '../forms';
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
interface IFilterField {
id?: string
keyword: string
whole_word: boolean
_destroy?: boolean
}
interface IEditFilter {
@ -28,7 +32,6 @@ const messages = defineMessages({
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' },
@ -43,18 +46,15 @@ const messages = defineMessages({
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' },
expiration_never: { id: 'colum.filters.expiration.never', defaultMessage: 'Never' },
expiration_1800: { id: 'colum.filters.expiration.1800', defaultMessage: '30 minutes' },
expiration_3600: { id: 'colum.filters.expiration.3600', defaultMessage: '1 hour' },
expiration_21600: { id: 'colum.filters.expiration.21600', defaultMessage: '6 hours' },
expiration_43200: { id: 'colum.filters.expiration.43200', defaultMessage: '12 hours' },
expiration_86400: { id: 'colum.filters.expiration.86400', defaultMessage: '1 day' },
expiration_604800: { id: 'colum.filters.expiration.604800', defaultMessage: '1 week' },
});
// 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();
@ -95,18 +95,28 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
const [notFound, setNotFound] = useState(false);
const [title, setTitle] = useState('');
const [expiresAt] = useState('');
const [expiresIn, setExpiresIn] = useState<string | null>(null);
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 [keywords, setKeywords] = useState<IFilterField[]>([{ keyword: '', whole_word: false }]);
// const handleSelectChange = e => {
// this.setState({ [e.target.name]: e.target.value });
// };
const expirations = useMemo(() => ({
'': intl.formatMessage(messages.expiration_never),
1800: intl.formatMessage(messages.expiration_1800),
3600: intl.formatMessage(messages.expiration_3600),
21600: intl.formatMessage(messages.expiration_21600),
43200: intl.formatMessage(messages.expiration_43200),
86400: intl.formatMessage(messages.expiration_86400),
604800: intl.formatMessage(messages.expiration_604800),
}), []);
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
setExpiresIn(e.target.value);
};
const handleAddNew: React.FormEventHandler = e => {
e.preventDefault();
@ -129,8 +139,8 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
}
dispatch(params.id
? updateFilter(params.id, title, expiresAt, context, hide, keywords)
: createFilter(title, expiresAt, context, hide, keywords)).then(() => {
? updateFilter(params.id, title, expiresIn, context, hide, keywords)
: createFilter(title, expiresIn, context, hide, keywords)).then(() => {
history.push('/filters');
}).catch(() => {
toast.error(intl.formatMessage(messages.create_error));
@ -141,7 +151,9 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
const handleAddKeyword = () => setKeywords(keywords => [...keywords, { keyword: '', whole_word: false }]);
const handleRemoveKeyword = (i: number) => setKeywords(keywords => keywords.filter((_, index) => index !== i));
const handleRemoveKeyword = (i: number) => setKeywords(keywords => keywords[i].id
? keywords.map((keyword, index) => index === i ? { ...keyword, _destroy: true } : keyword)
: keywords.filter((_, index) => index !== i));
useEffect(() => {
if (params.id) {
@ -180,13 +192,16 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
onChange={({ target }) => setTitle(target.value)}
/>
</FormGroup>
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
{features.filtersExpiration && (
<FormGroup labelText={intl.formatMessage(messages.expires)}>
<SelectDropdown
items={expirations}
defaultValue={expirations.never}
onChange={this.handleSelectChange}
defaultValue={''}
onChange={handleSelectChange}
/>
</FormGroup> */}
</FormGroup>
)}
<Stack>
<Text size='sm' weight='medium'>

View File

@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import { fetchFilters, deleteFilter } from 'soapbox/actions/filters';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Button, CardTitle, Column, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
import { Button, CardTitle, Column, HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import toast from 'soapbox/toast';
@ -31,6 +31,7 @@ const messages = defineMessages({
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
subheading_filters: { id: 'column.filters.subheading_filters', defaultMessage: 'Current Filters' },
edit: { id: 'column.filters.edit', defaultMessage: 'Edit' },
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
});
@ -42,16 +43,6 @@ const contexts = {
account: messages.accounts,
};
// const expirations = {
// null: 'Never',
// // 1800: '30 minutes',
// // 3600: '1 hour',
// // 21600: '6 hour',
// // 43200: '12 hours',
// // 86400 : '1 day',
// // 604800: '1 week',
// };
const Filters = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -94,10 +85,11 @@ const Filters = () => {
itemClassName='pb-4 last:pb-0'
>
{filters.map((filter, i) => (
<HStack space={1}>
<div className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
<Stack space={2}>
<Stack className='grow' space={1}>
<Text weight='medium'>
<FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' />
<FormattedMessage id='filters.filters_list_phrases_label' defaultMessage='Keywords or phrases:' />
{' '}
<Text theme='muted' tag='span'>{filter.keywords.map(keyword => keyword.keyword).join(', ')}</Text>
</Text>
@ -119,19 +111,16 @@ 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')}
onClick={handleFilterDelete(filter.id)}
title={intl.formatMessage(messages.delete)}
/>
<HStack space={2} justifyContent='end'>
<Button theme='primary' onClick={handleFilterEdit(filter.id)}>
{intl.formatMessage(messages.edit)}
</Button>
<Button theme='danger' onClick={handleFilterDelete(filter.id)}>
{intl.formatMessage(messages.delete)}
</Button>
</HStack>
</Stack>
</div>
))}
</ScrollableList>
</Column>

View File

@ -314,7 +314,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* Mastodon's newer solution for direct messaging.
* @see {@link https://docs.joinmastodon.org/methods/timelines/conversations/}
* @see {@link https://docs.joinmastodon.org/methods/conversations/}
*/
conversations: any([
v.software === FRIENDICA,
@ -450,6 +450,12 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA,
]),
/** Whether filters can automatically expires. */
filtersExpiration: any([
v.software === MASTODON,
v.software === PLEROMA && gte(v.version, '2.3.0'),
]),
/**
* Can edit and manage timeline filters (aka "muted words").
* @see {@link https://docs.joinmastodon.org/methods/filters/}
@ -458,7 +464,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* Allows setting the focal point of a media attachment.
* @see {@link https://docs.joinmastodon.org/methods/statuses/media/}
* @see {@link https://docs.joinmastodon.org/methods/media/}
*/
focalPoint: v.software === MASTODON && gte(v.compatVersion, '2.3.0'),
@ -529,7 +535,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* Can create, view, and manage lists.
* @see {@link https://docs.joinmastodon.org/methods/timelines/lists/}
* @see {@link https://docs.joinmastodon.org/methods/lists/}
* @see GET /api/v1/timelines/list/:list_id
*/
lists: any([
@ -644,7 +650,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* A directory of discoverable profiles from the instance.
* @see {@link https://docs.joinmastodon.org/methods/instance/directory/}
* @see {@link https://docs.joinmastodon.org/methods/directory/}
*/
profileDirectory: any([
v.software === FRIENDICA,
@ -736,7 +742,7 @@ const getInstanceFeatures = (instance: Instance) => {
/**
* Can schedule statuses to be posted at a later time.
* @see POST /api/v1/statuses
* @see {@link https://docs.joinmastodon.org/methods/statuses/scheduled_statuses/}
* @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/}
*/
scheduledStatuses: any([
v.software === MASTODON && gte(v.version, '2.7.0'),