From 1d4d9c2732cbaab677eacc6035589a7cf406d859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 5 Mar 2023 19:49:40 +0100 Subject: [PATCH] Filters expiration, restyle filters list, fix keywords deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/filters.ts | 28 +++---- .../components/ui/streamfield/streamfield.tsx | 2 +- .../features/auth-token-list/index.tsx | 7 +- app/soapbox/features/filters/edit-filter.tsx | 69 +++++++++------- app/soapbox/features/filters/index.tsx | 81 ++++++++----------- app/soapbox/utils/features.ts | 16 ++-- 6 files changed, 106 insertions(+), 97 deletions(-) diff --git a/app/soapbox/actions/filters.ts b/app/soapbox/actions/filters.ts index c50d59818..ee3508682 100644 --- a/app/soapbox/actions/filters.ts +++ b/app/soapbox/actions/filters.ts @@ -147,7 +147,7 @@ const fetchFilter = (id: string) => if (features.filters) return dispatch(fetchFilterV1(id)); }; -const createFilterV1 = (title: string, expires_at: string, context: Array, hide: boolean, keywords: FilterKeywords) => +const createFilterV1 = (title: string, expires_in: string | null, context: Array, 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 { 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, hide: boolean, keywords_attributes: FilterKeywords) => +const createFilterV2 = (title: string, expires_in: string | null, context: Array, 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, hide: boolean, keywords: FilterKeywords) => +const createFilter = (title: string, expires_in: string | null, context: Array, 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, hide: boolean, keywords: FilterKeywords) => +const updateFilterV1 = (id: string, title: string, expires_in: string | null, context: Array, 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, hide: boolean, keywords_attributes: FilterKeywords) => +const updateFilterV2 = (id: string, title: string, expires_in: string | null, context: Array, 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, hide: boolean, keywords: FilterKeywords) => +const updateFilter = (id: string, title: string, expires_in: string | null, context: Array, 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) => diff --git a/app/soapbox/components/ui/streamfield/streamfield.tsx b/app/soapbox/components/ui/streamfield/streamfield.tsx index 3599eaae1..5c436e70b 100644 --- a/app/soapbox/components/ui/streamfield/streamfield.tsx +++ b/app/soapbox/components/ui/streamfield/streamfield.tsx @@ -70,7 +70,7 @@ const Streamfield: React.FC = ({ {(values.length > 0) && ( - {values.map((value, i) => ( + {values.map((value, i) => value?._destroy ? null : ( {values.length > minItems && onRemoveItem && ( diff --git a/app/soapbox/features/auth-token-list/index.tsx b/app/soapbox/features/auth-token-list/index.tsx index 9904f3f64..a97d7ef75 100644 --- a/app/soapbox/features/auth-token-list/index.tsx +++ b/app/soapbox/features/auth-token-list/index.tsx @@ -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 = ({ token, isCurrent }) => { )} - -
+ -
+ ); diff --git a/app/soapbox/features/filters/edit-filter.tsx b/app/soapbox/features/filters/edit-filter.tsx index 24c3bdfe4..207195387 100644 --- a/app/soapbox/features/filters/edit-filter.tsx +++ b/app/soapbox/features/filters/edit-filter.tsx @@ -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 = ({ value, onChange }) => { const intl = useIntl(); @@ -95,18 +95,28 @@ const EditFilter: React.FC = ({ params }) => { const [notFound, setNotFound] = useState(false); const [title, setTitle] = useState(''); - const [expiresAt] = useState(''); + const [expiresIn, setExpiresIn] = useState(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([{ 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 = e => { + setExpiresIn(e.target.value); + }; const handleAddNew: React.FormEventHandler = e => { e.preventDefault(); @@ -129,8 +139,8 @@ const EditFilter: React.FC = ({ 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 = ({ 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 = ({ params }) => { onChange={({ target }) => setTitle(target.value)} /> - {/* - - */} + + {features.filtersExpiration && ( + + + + )} diff --git a/app/soapbox/features/filters/index.tsx b/app/soapbox/features/filters/index.tsx index d1cbe1f3b..6b09eb515 100644 --- a/app/soapbox/features/filters/index.tsx +++ b/app/soapbox/features/filters/index.tsx @@ -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,44 +85,42 @@ const Filters = () => { itemClassName='pb-4 last:pb-0' > {filters.map((filter, i) => ( - - - - - {' '} - {filter.keywords.map(keyword => keyword.keyword).join(', ')} - - - - {' '} - {filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')} - - - {/* - {filter.irreversible ? - : - } +
+ + + + + {' '} + {filter.keywords.map(keyword => keyword.keyword).join(', ')} - {filter.whole_word && ( - - - - )} */} + + + {' '} + {filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')} + + + {/* + {filter.irreversible ? + : + } + + {filter.whole_word && ( + + + + )} */} + + + + + - - - +
))} diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index c818ccaab..c0ae18d27 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -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'),