From 207b73b8375809cb4cc2d7f1fcccec4de7885d19 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 13 Mar 2025 01:55:55 -0500 Subject: [PATCH] Rework the data layer --- src/features/compose/components/search.tsx | 12 +- .../explore/components/exploreFilter.tsx | 93 +++-- src/features/explore/components/filters.tsx | 323 ++++++------------ src/features/explore/index.tsx | 52 +-- src/features/explore/useSearchTokens.ts | 63 ++++ src/features/forms/index.tsx | 1 + src/reducers/index.ts | 2 - src/reducers/search-filter.ts | 164 --------- 8 files changed, 267 insertions(+), 443 deletions(-) create mode 100644 src/features/explore/useSearchTokens.ts delete mode 100644 src/reducers/search-filter.ts diff --git a/src/features/compose/components/search.tsx b/src/features/compose/components/search.tsx index 4b0bb32f8..a93430ff4 100644 --- a/src/features/compose/components/search.tsx +++ b/src/features/compose/components/search.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import { debounce } from 'es-toolkit'; import { useCallback, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { changeSearch, @@ -17,7 +17,6 @@ import { import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input.tsx'; import Input from 'soapbox/components/ui/input.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; -import { formatFilters } from 'soapbox/features/explore/components/exploreFilter.tsx'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { selectAccount } from 'soapbox/selectors/index.ts'; @@ -57,12 +56,9 @@ const Search = (props: ISearch) => { const history = useHistory(); const intl = useIntl(); const [inputValue, setInputValue] = useState(''); - const path = useLocation().pathname; const value = useAppSelector((state) => state.search.value); const submitted = useAppSelector((state) => state.search.submitted); - const filters = useAppSelector((state) => state.search_filter); - const formatFiltersString = formatFilters(filters); const debouncedSubmit = useCallback(debounce(() => { dispatch(submitSearch()); @@ -71,11 +67,7 @@ const Search = (props: ISearch) => { const handleChange = (event: React.ChangeEvent) => { const { value } = event.target; - if (formatFiltersString.length > 0 && path === '/explore') { - dispatch(changeSearch(`${formatFiltersString} ${value}`)); - } else { - dispatch(changeSearch(value)); - } + dispatch(changeSearch(value)); setInputValue(value); if (autoSubmit) { diff --git a/src/features/explore/components/exploreFilter.tsx b/src/features/explore/components/exploreFilter.tsx index c323048ae..f91704532 100644 --- a/src/features/explore/components/exploreFilter.tsx +++ b/src/features/explore/components/exploreFilter.tsx @@ -1,7 +1,7 @@ import arrowIcon from '@tabler/icons/outline/chevron-down.svg'; import { debounce } from 'es-toolkit'; import { useEffect, useMemo, useState } from 'react'; -import { useIntl } from 'react-intl'; +// import { useIntl } from 'react-intl'; import { changeSearch, submitSearch } from 'soapbox/actions/search.ts'; import Divider from 'soapbox/components/ui/divider.tsx'; @@ -9,16 +9,14 @@ import HStack from 'soapbox/components/ui/hstack.tsx'; import IconButton from 'soapbox/components/ui/icon-button.tsx'; import Stack from 'soapbox/components/ui/stack.tsx'; import { - CreateFilter, + WordFilter, LanguageFilter, MediaFilter, PlatformFilters, ToggleRepliesFilter, - generateFilter, } from 'soapbox/features/explore/components/filters.tsx'; +import { useSearchTokens } from 'soapbox/features/explore/useSearchTokens.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; -import { IFilters } from 'soapbox/reducers/search-filter.ts'; interface IGenerateFilter { name: string; @@ -26,19 +24,10 @@ interface IGenerateFilter { value: string; } -export const formatFilters = (filters: IFilters[]): string => { - const language = filters[0].name.toLowerCase() !== 'default' ? filters[0].value : ''; - const protocols = filters.slice(1, 4).filter((protocol) => !protocol.status).map((filter) => filter.value).join(' '); - const defaultFilters = filters.slice(4, 8).filter((x) => x.status).map((filter) => filter.value).join(' '); - const newFilters = filters.slice(8).map((searchFilter) => searchFilter.value).join(' '); - - return [language, protocols, defaultFilters, newFilters].join(' ').trim(); -}; - const ExploreFilter = () => { + // const intl = useIntl(); const dispatch = useAppDispatch(); - const filters = useAppSelector((state) => state.search_filter); - const intl = useIntl(); + const { tokens } = useSearchTokens(); const [isOpen, setIsOpen] = useState(false); const handleClick = () => { @@ -59,14 +48,13 @@ const ExploreFilter = () => { useEffect( () => { - const value = formatFilters(filters); - debouncedSearch(value); + debouncedSearch([...tokens].join(' ')); return () => { debouncedSearch.cancel(); }; - }, [filters, dispatch], + }, [tokens, dispatch], ); useEffect( @@ -84,7 +72,7 @@ const ExploreFilter = () => { {/* Filters */} - {filters.length > 0 && [...filters.slice(0, 8).filter((value) => value.status).map((value) => generateFilter(dispatch, intl, value)), ...filters.slice(8).map((value) => generateFilter(dispatch, intl, value))]} + {/* {tokens.size > 0 && [...filters.slice(0, 8).filter((value) => value.status).map((value) => generateFilter(dispatch, intl, value)), ...filters.slice(8).map((value) => generateFilter(dispatch, intl, value))]} */} { {/* Create your filter */} - + @@ -120,6 +108,69 @@ const ExploreFilter = () => { ); }; +// const generateFilter = (dispatch: AppDispatch, intl: IntlShape, { name, value, status }: IGenerateFilter) => { +// let borderColor = ''; +// let textColor = ''; + +// if (Object.keys(languages).some((lang) => value.includes('language:'))) { +// borderColor = 'border-gray-500'; +// textColor = 'text-gray-500'; +// } else { +// switch (value) { +// case 'reply:false': +// case 'media:true -video:true': +// case 'video:true': +// case '-media:true': +// borderColor = 'border-gray-500'; +// textColor = 'text-gray-500'; +// break; +// case 'protocol:nostr': +// borderColor = 'border-purple-500'; +// textColor = 'text-purple-500'; +// break; +// case 'protocol:atproto': +// borderColor = 'border-blue-500'; +// textColor = 'text-blue-500'; +// break; +// case 'protocol:activitypub': +// borderColor = 'border-indigo-500'; +// textColor = 'text-indigo-500'; +// break; +// default: +// borderColor = status ? 'border-green-500' : 'border-red-500'; +// textColor = status ? 'text-green-500' : 'text-red-500'; +// } +// } + +// const handleChangeFilters = (e: React.MouseEvent) => { +// e.stopPropagation(); + +// if (['protocol:nostr', 'protocol:atproto', 'protocol:activitypub'].includes(value)) { +// dispatch(selectProtocol(value)); +// } else if (['reply:false', 'media:true -video:true', 'video:true', '-media:true'].includes(value)) { +// dispatch(changeStatus({ value: value, status: false })); +// } else if (value.includes('language:')) { +// dispatch(changeLanguage('default')); +// } else { +// dispatch(removeFilter(value)); +// } +// }; + +// return ( +//
+// {name.toLowerCase() !== 'default' ? name : } +// +//
+// ); +// }; + export default ExploreFilter; export type { IGenerateFilter }; \ No newline at end of file diff --git a/src/features/explore/components/filters.tsx b/src/features/explore/components/filters.tsx index 35bddab03..bf2d3ca22 100644 --- a/src/features/explore/components/filters.tsx +++ b/src/features/explore/components/filters.tsx @@ -2,8 +2,8 @@ import refreshIcon from '@tabler/icons/outline/refresh.svg'; import searchIcon from '@tabler/icons/outline/search.svg'; import xIcon from '@tabler/icons/outline/x.svg'; import clsx from 'clsx'; -import React, { useEffect, useMemo, useState } from 'react'; -import { FormattedMessage, IntlShape, defineMessages, useIntl } from 'react-intl'; +import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import Button from 'soapbox/components/ui/button.tsx'; import Checkbox from 'soapbox/components/ui/checkbox.tsx'; @@ -14,29 +14,22 @@ import Stack from 'soapbox/components/ui/stack.tsx'; import SvgIcon from 'soapbox/components/ui/svg-icon.tsx'; import Text from 'soapbox/components/ui/text.tsx'; import Toggle from 'soapbox/components/ui/toggle.tsx'; -import { IGenerateFilter } from 'soapbox/features/explore/components/exploreFilter.tsx'; +import { useSearchTokens } from 'soapbox/features/explore/useSearchTokens.ts'; import { SelectDropdown } from 'soapbox/features/forms/index.tsx'; -import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; -import { changeStatus, changeLanguage, changeMedia, createFilter, removeFilter, selectProtocol, resetFilters } from 'soapbox/reducers/search-filter.ts'; -import { AppDispatch, RootState } from 'soapbox/store.ts'; import toast from 'soapbox/toast.tsx'; const messages = defineMessages({ - noReplies: { id: 'column.explore.filters.no_replies', defaultMessage: 'No Replies:' }, + showReplies: { id: 'home.column_settings.show_replies', defaultMessage: 'Show replies' }, media: { id: 'column.explore.filters.media', defaultMessage: 'Media:' }, language: { id: 'column.explore.filters.language', defaultMessage: 'Language:' }, platforms: { id: 'column.explore.filters.platforms', defaultMessage: 'Platforms:' }, - platformsError: { id: 'column.explore.filters.platforms.error', defaultMessage: 'Protocol not found for: {name}' }, - atLeast: { id: 'column.explore.filters.atLeast', defaultMessage: 'At least one platform must remain selected.' }, createYourFilter: { id: 'column.explore.filters.create_your_filter', defaultMessage: 'Create your filter' }, resetFilter: { id: 'column.explore.filters.reset', defaultMessage: 'Reset Filters' }, filterByWords: { id: 'column.explore.filters.filter_by_words', defaultMessage: 'Filter by this/these words' }, - include: { id: 'column.explore.filters.include', defaultMessage: 'Include' }, - exclude: { id: 'column.explore.filters.exclude', defaultMessage: 'Exclude' }, + negative: { id: 'column.explore.filters.invert', defaultMessage: 'Invert' }, nostr: { id: 'column.explore.filters.nostr', defaultMessage: 'Nostr' }, - bluesky: { id: 'column.explore.filters.bluesky', defaultMessage: 'Bluesky' }, - fediverse: { id: 'column.explore.filters.fediverse', defaultMessage: 'Fediverse' }, + atproto: { id: 'column.explore.filters.bluesky', defaultMessage: 'Bluesky' }, + activitypub: { id: 'column.explore.filters.fediverse', defaultMessage: 'Fediverse' }, cancel: { id: 'column.explore.filters.cancel', defaultMessage: 'Cancel' }, addFilter: { id: 'column.explore.filters.add_filter', defaultMessage: 'Add Filter' }, all: { id: 'column.explore.media_filters.all', defaultMessage: 'All' }, @@ -107,63 +100,43 @@ const languages = { zh: '中文', }; -const PlatformFilters = () => { +const ProtocolCheckBox: React.FC<{ protocol: 'nostr' | 'atproto' | 'activitypub' }> = ({ protocol }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const filterList = useAppSelector((state: RootState) => state.search_filter); + const { tokens, addToken, removeToken } = useSearchTokens(); + + const token = `-protocol:${protocol}`; + const checked = !tokens.has(token); + const message = messages[protocol]; const handleProtocolFilter = (e: React.ChangeEvent) => { - const isChecked = e.target.checked; - const matchingFilter = filterList.slice(1, 4).find( - (filter) => filter.name.toLowerCase() === e.target.name.toLowerCase(), - ); + const { checked, name } = e.target; - const protocol = matchingFilter?.value; - const isLastChecked = filterList.slice(1, 4).filter((filter) => filter.status).length === 1; + const token = `-protocol:${name}`; - if (!isChecked && isLastChecked) { - toast.error(messages.atLeast); - return; + if (checked) { + removeToken(token); + } else { + addToken(token); } - - if (!protocol) { - console.error(intl.formatMessage(messages.platformsError, { name: e.target.name })); - return; - } - - dispatch(selectProtocol(protocol)); }; - const CustomCheckBox = ({ protocolN } : { protocolN: string }) => { - const filter = filterList.find((filter) => filter.name.toLowerCase() === protocolN); - const checked = filter?.status; - let message; + return ( + + + + {intl.formatMessage(message)} + + + ); +}; - switch (protocolN) { - case 'nostr': - message = messages.nostr; - break; - case 'bluesky': - message = messages.bluesky; - break; - default: - message = messages.fediverse; - } - - return ( - - - - {intl.formatMessage(message)} - - - ); - }; +const PlatformFilters = () => { + const intl = useIntl(); return ( @@ -171,39 +144,33 @@ const PlatformFilters = () => { {intl.formatMessage(messages.platforms)} - {/* Nostr */} - - - {/* Bluesky */} - - - {/* Fediverse */} - - + + + ); }; -const CreateFilter = () => { +const WordFilter = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { addToken, clearTokens } = useSearchTokens(); - const [inputValue, setInputValue] = useState(''); - const [include, setInclude] = useState(true); - const hasValue = inputValue.length > 0; + const [word, setWord] = useState(''); + const [negative, setNegative] = useState(false); + const hasValue = !!word; const handleReset = () => { - dispatch(resetFilters()); + clearTokens(); }; const handleClearValue = () => { - setInputValue(''); + setWord(''); }; const handleAddFilter = () => { - if (inputValue.length > 0) { - dispatch(createFilter({ name: inputValue, status: include })); + if (word) { + addToken(`${negative ? '-' : ''}${word}`); handleClearValue(); } else { toast.error(intl.formatMessage(messages.empty)); @@ -228,7 +195,7 @@ const CreateFilter = () => { }; const handleOnChange = (e: React.ChangeEvent) => { - setInputValue(e.target.value); + setWord(e.target.value); }; return ( @@ -247,10 +214,8 @@ const CreateFilter = () => { - -
- +
{ setInputValue('')} + onClick={() => setWord('')} aria-label={intl.formatMessage(messages.clearSearch)} className={clsx('size-4 text-gray-600', { hidden: !hasValue })} />
-
{/* Include */} { - if (!include) { - setInclude(true); - } - }} + name='negative' + checked={negative} + onChange={() => setNegative(!negative)} /> - {intl.formatMessage(messages.include)} - - - - {/* Exclude */} - - { - if (include) { - setInclude(false); - } - }} - /> - - {intl.formatMessage(messages.exclude)} + {intl.formatMessage(messages.negative)}
@@ -324,32 +268,45 @@ const CreateFilter = () => { const MediaFilter = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const filters = useAppSelector((state) => state.search_filter.filter(filter => ['all', 'image only', 'video only', 'no media'].includes(filter.name.toLowerCase()))); + const { tokens, addTokens, removeTokens } = useSearchTokens(); - - const mediaFilters = useMemo(() => ({ - all: intl.formatMessage(messages.all), - image: intl.formatMessage(messages.imageOnly), - video: intl.formatMessage(messages.videoOnly), - none: intl.formatMessage(messages.none), - }), [intl]); - - const [selectedMedia, setSelectedMedia] = useState(mediaFilters.all); - - useEffect(() => { - const newMediaValue = (Object.keys(mediaFilters) as Array) - .find((key) => mediaFilters[key] === filters.find((filter) => filter.status === true)?.name) - || mediaFilters.all; - - setSelectedMedia(newMediaValue); - }, [mediaFilters]); + const mediaFilters = { + all: { + tokens: [], + label: intl.formatMessage(messages.all), + }, + image: { + tokens: ['media:true', '-video:true'], + label: intl.formatMessage(messages.imageOnly), + }, + video: { + tokens: ['video:true'], + label: intl.formatMessage(messages.videoOnly), + }, + none: { + tokens: ['-media:true'], + label: intl.formatMessage(messages.none), + }, + }; const handleSelectChange: React.ChangeEventHandler = e => { - const filter = e.target.value; - dispatch(changeMedia(filter)); + const filter = e.target.value as keyof typeof mediaFilters; + removeTokens(['media:true', '-video:true', 'video:true', '-media:true']); + addTokens(mediaFilters[filter].tokens); }; + // FIXME: The `items` prop of `SelectDropdown` should become an array of objects. + const items = Object + .entries(mediaFilters) + .reduce((acc, [key, value]) => { + acc[key] = value.label; + return acc; + }, {} as Record); + + const currentFilter = Object + .entries(mediaFilters) + .find(([, f]) => f.tokens.every(token => tokens.has(token)))?.[0] || 'all'; + return ( @@ -357,10 +314,9 @@ const MediaFilter = () => { @@ -370,14 +326,23 @@ const MediaFilter = () => { const LanguageFilter = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const filter = useAppSelector((state) => state.search_filter)[0]; + const { tokens, addToken, removeToken } = useSearchTokens(); const handleSelectChange: React.ChangeEventHandler = e => { const language = e.target.value; - dispatch(changeLanguage(language)); + + for (const token in tokens) { + if (token.startsWith('language:')) { + removeToken(token); + } + } + + addToken(`language:${language}`); }; + const token = [...tokens].find((token) => token.startsWith('language:')); + const [, language = 'default'] = token?.split(':') ?? []; + return ( @@ -385,10 +350,9 @@ const LanguageFilter = () => { @@ -398,91 +362,28 @@ const LanguageFilter = () => { const ToggleRepliesFilter = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const label = intl.formatMessage(messages.noReplies); - const filters = useAppSelector((state) => state.search_filter); - const repliesFilter = filters.find((filter) => filter.value.toLowerCase().includes('reply')); - const checked = repliesFilter?.status; + const { tokens, addToken, removeToken } = useSearchTokens(); const handleToggle = () => { - dispatch(changeStatus({ value: 'reply:false', status: !checked })); + if (tokens.has('reply:false')) { + removeToken('reply:false'); + } else { + addToken('reply:false'); + } }; return ( - {label} + {intl.formatMessage(messages.showReplies)} ); }; -const generateFilter = (dispatch: AppDispatch, intl: IntlShape, { name, value, status }: IGenerateFilter) => { - let borderColor = ''; - let textColor = ''; - - if (Object.keys(languages).some((lang) => value.includes('language:'))) { - borderColor = 'border-gray-500'; - textColor = 'text-gray-500'; - } else { - switch (value) { - case 'reply:false': - case 'media:true -video:true': - case 'video:true': - case '-media:true': - borderColor = 'border-gray-500'; - textColor = 'text-gray-500'; - break; - case 'protocol:nostr': - borderColor = 'border-purple-500'; - textColor = 'text-purple-500'; - break; - case 'protocol:atproto': - borderColor = 'border-blue-500'; - textColor = 'text-blue-500'; - break; - case 'protocol:activitypub': - borderColor = 'border-indigo-500'; - textColor = 'text-indigo-500'; - break; - default: - borderColor = status ? 'border-green-500' : 'border-red-500'; - textColor = status ? 'text-green-500' : 'text-red-500'; - } - } - - const handleChangeFilters = (e: React.MouseEvent) => { - e.stopPropagation(); - - if (['protocol:nostr', 'protocol:atproto', 'protocol:activitypub'].includes(value)) { - dispatch(selectProtocol(value)); - } else if (['reply:false', 'media:true -video:true', 'video:true', '-media:true'].includes(value)) { - dispatch(changeStatus({ value: value, status: false })); - } else if (value.includes('language:')) { - dispatch(changeLanguage('default')); - } else { - dispatch(removeFilter(value)); - } - }; - - return ( -
- {name.toLowerCase() !== 'default' ? name : } - -
- ); -}; - -export { CreateFilter, PlatformFilters, MediaFilter, LanguageFilter, ToggleRepliesFilter, generateFilter }; \ No newline at end of file +export { WordFilter, PlatformFilters, MediaFilter, LanguageFilter, ToggleRepliesFilter }; \ No newline at end of file diff --git a/src/features/explore/index.tsx b/src/features/explore/index.tsx index 14550ee6e..c5ac8bef2 100644 --- a/src/features/explore/index.tsx +++ b/src/features/explore/index.tsx @@ -16,11 +16,10 @@ import Search from 'soapbox/features/compose/components/search.tsx'; import ExploreCards from 'soapbox/features/explore/components/explore-cards.tsx'; import ExploreFilter from 'soapbox/features/explore/components/exploreFilter.tsx'; import AccountsCarousel from 'soapbox/features/explore/components/popular-accounts.tsx'; +import { useSearchTokens } from 'soapbox/features/explore/useSearchTokens.ts'; import { PublicTimeline } from 'soapbox/features/ui/util/async-components.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; -import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useFeatures } from 'soapbox/hooks/useFeatures.ts'; -import { IFilters, initialState as filterInitialState } from 'soapbox/reducers/search-filter.ts'; import { SearchFilter } from 'soapbox/reducers/search.ts'; const messages = defineMessages({ @@ -31,45 +30,28 @@ const messages = defineMessages({ filters: { id: 'column.explore.filters', defaultMessage: 'Filters:' }, }); -const checkFilters = (filters: IFilters[]) => { - return filters.length !== filterInitialState.length || - !filters.every((filter, index) => - filter.name === filterInitialState[index].name && - filter.status === filterInitialState[index].status && - filter.value === filterInitialState[index].value, - ); -}; - const PostsTab = () => { - const path = useLocation().pathname; const intl = useIntl(); - const inPosts = path === '/explore'; - const filters = useAppSelector((state) => state.search_filter); - const isNostr = useFeatures().nostr; - - const [withFilter, setWithFilter] = useState(checkFilters(filters)); - - useEffect(() => { - setWithFilter(checkFilters(filters)); - }, [filters]); + const features = useFeatures(); + const { tokens } = useSearchTokens(); + const { pathname } = useLocation(); return ( - {inPosts && <> + {pathname === '/explore' && ( + <> + {features.nostr && ( + <> + + + + + + )} - {isNostr && <> - - - - - - - - } - - {!withFilter ? : } - - } + {tokens.size ? : } + + )} ); diff --git a/src/features/explore/useSearchTokens.ts b/src/features/explore/useSearchTokens.ts new file mode 100644 index 000000000..9ccd7634f --- /dev/null +++ b/src/features/explore/useSearchTokens.ts @@ -0,0 +1,63 @@ +import { produce, enableMapSet } from 'immer'; +import { create } from 'zustand'; + +enableMapSet(); + +interface SearchTokensState { + tokens: Set; + addToken(token: string): void; + addTokens(tokens: string[]): void; + removeToken(token: string): void; + removeTokens(tokens: string[]): void; + clearTokens(): void; +} + +export const useSearchTokens = create()( + (setState) => ({ + tokens: new Set(), + + addToken(token: string): void { + setState((state) => { + return produce(state, (draft) => { + draft.tokens.add(token); + }); + }); + }, + + addTokens(tokens: string[]): void { + setState((state) => { + return produce(state, (draft) => { + for (const token of tokens) { + draft.tokens.add(token); + } + }); + }); + }, + + removeToken(token: string): void { + setState((state) => { + return produce(state, (draft) => { + draft.tokens.delete(token); + }); + }); + }, + + removeTokens(tokens: string[]): void { + setState((state) => { + return produce(state, (draft) => { + for (const token of tokens) { + draft.tokens.delete(token); + } + }); + }); + }, + + clearTokens(): void { + setState((state) => { + return produce(state, (draft) => { + draft.tokens.clear(); + }); + }); + }, + }), +); diff --git a/src/features/forms/index.tsx b/src/features/forms/index.tsx index d7f9d15d4..3468171c4 100644 --- a/src/features/forms/index.tsx +++ b/src/features/forms/index.tsx @@ -120,6 +120,7 @@ interface ISelectDropdown { className?: string; label?: React.ReactNode; hint?: React.ReactNode; + /** @deprecated FIXME: JavaScript does not guarantee key ordering of objects. This should be turned into an array of tuples. */ items: Record; defaultValue?: string; onChange?: React.ChangeEventHandler; diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 031f6336b..e57877462 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -41,7 +41,6 @@ import profile_hover_card from './profile-hover-card.ts'; import relationships from './relationships.ts'; import reports from './reports.ts'; import scheduled_statuses from './scheduled-statuses.ts'; -import search_filter from './search-filter.ts'; import search from './search.ts'; import security from './security.ts'; import settings from './settings.ts'; @@ -99,7 +98,6 @@ export default combineReducers({ reports, scheduled_statuses, search, - search_filter, security, settings, sidebar, diff --git a/src/reducers/search-filter.ts b/src/reducers/search-filter.ts deleted file mode 100644 index fdc977b1c..000000000 --- a/src/reducers/search-filter.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; - -interface IFilters { - name: string; - status: boolean; - value: string; -} - -interface IToggle { - value: string; - status: boolean; -} - -interface INewFilter { - name: string; - status: boolean; -} - -const initialState: IFilters[] = [ - { name: 'default', status: false, value: 'language:default' }, - { name: 'Nostr', status: true, value: 'protocol:nostr' }, - { name: 'Bluesky', status: true, value: 'protocol:atproto' }, - { name: 'Fediverse', status: true, value: 'protocol:activitypub' }, - { name: 'No Replies', status: false, value: 'reply:false' }, - { name: 'Video only', status: false, value: 'video:true' }, - { name: 'Image only', status: false, value: 'media:true -video:true' }, - { name: 'No media', status: false, value: '-media:true' }, -]; - -const search_filter = createSlice({ - name: 'search_filter', - initialState, - reducers: { - /** - * Toggles the status of reply filter. - */ - changeStatus: (state, action: PayloadAction) => { - return state.map((currentState) => { - const status = action.payload.status; - const value = action.payload.value; - return currentState.value === value - ? { - ...currentState, - status: status, - } - : currentState; - }); - }, - - /** - * Changes the media filter. - */ - changeMedia: (state, action: PayloadAction) => { - const selected = action.payload.toLowerCase(); - - const resetMediaState = state.map((currentFilter, index) => { - return index > 3 && index <= 7 - ? { - ...currentFilter, - status: false, - } - : currentFilter; - }); - - const applyMediaFilter = (searchFilter: string) => { - return resetMediaState.map((currentState) => - currentState.name.toLowerCase().includes(searchFilter) - ? { - ...currentState, - status: true, - } - : currentState, - ); - }; - - switch (selected) { - case 'text': - case 'video': - case 'image': - return applyMediaFilter(`${selected} only`); - case 'none': - return applyMediaFilter('no media'); - default: - return resetMediaState; - } - }, - - /** - * Changes the language filter. - */ - changeLanguage: (state, action: PayloadAction) => { - const selected = action.payload.toLowerCase(); - const isDefault = selected === 'default'; - return state.map((currentState) => - currentState.value.includes('language:') - ? { - name: isDefault ? selected : selected.toUpperCase(), - status: !isDefault, - value: `language:${selected}`, - } - : currentState, - ); - }, - - /** - * Toggles the status of a protocol filter. - */ - selectProtocol: (state, action: PayloadAction) => { - const protocol = action.payload; - return state.map((currentState) => { - if (currentState.value.toLowerCase() !== protocol) return currentState; - - const newStatus = !currentState.status; - let newValue = currentState.value; - - if (newStatus) { - newValue = currentState.value.startsWith('-') - ? currentState.value.slice(1) - : currentState.value; - } else { - newValue = currentState.value.startsWith('-') - ? currentState.value - : `-${currentState.value}`; - } - - return { - ...currentState, - status: newStatus, - value: newValue, - }; - }); - }, - - /** - * Creates a new filter. - */ - createFilter: (state, action: PayloadAction) => { - const filterWords = action.payload.name.trim(); - const status = action.payload.status; - const value = status ? filterWords : `-${filterWords.split(' ').join(' -')}`; - if (state.slice(7).some((currentState) => currentState.name === filterWords)) { - return state; - } - return [...state, { name: filterWords, status, value }]; - }, - - /** - * Removes a filter. - */ - removeFilter: (state, action: PayloadAction) => { - return state.filter((filter) => filter.value !== action.payload); - }, - - /** - * Resets the filters to the initial state. - */ - resetFilters: () => initialState, - }, -}); - -export type { IFilters }; -export { initialState }; -export const { changeStatus, changeMedia, changeLanguage, selectProtocol, createFilter, removeFilter, resetFilters } = search_filter.actions; -export default search_filter.reducer; \ No newline at end of file