diff --git a/src/app.tsx b/src/app.tsx index 3843ee3b..b3810f16 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -55,8 +55,9 @@ const BoardLayout = () => { {isMobile ? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( <> - +
+ ) : (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( diff --git a/src/components/board-buttons/board-buttons.module.css b/src/components/board-buttons/board-buttons.module.css index 43068ba4..59d754e4 100644 --- a/src/components/board-buttons/board-buttons.module.css +++ b/src/components/board-buttons/board-buttons.module.css @@ -51,7 +51,6 @@ } .desktopBoardButtons { - padding-right: 5px; text-transform: capitalize; } diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 073a504d..320a414c 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -4,17 +4,18 @@ import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; -import useTimeFilter from '../../hooks/use-time-filter'; -import CatalogFilters from '../../views/catalog/catalog-filters/'; -import styles from './board-buttons.module.css'; -import Tooltip from '../tooltip'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; -import _ from 'lodash'; import useIsMobile from '../../hooks/use-is-mobile'; -import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; +import useTimeFilter from '../../hooks/use-time-filter'; +import CatalogFilters from '../catalog-filters'; +import CatalogSearch from '../catalog-search'; +import Tooltip from '../tooltip'; +import styles from './board-buttons.module.css'; +import _ from 'lodash'; interface BoardButtonsProps { address?: string | undefined; @@ -282,7 +283,7 @@ export const MobileBoardButtons = () => {
- +
@@ -380,17 +381,16 @@ export const DesktopBoardButtons = () => { {(isInAllView || isInSubscriptionsView) && ( )} - {isInCatalogView && ( - <> - [ - ] - - )}{' '} {!(isInAllView || isInSubscriptionsView) && ( <> [ ] + )}{' '} + {isInCatalogView && ( + <> + [] + )} diff --git a/src/views/catalog/catalog-filters/catalog-filters.module.css b/src/components/catalog-filters/catalog-filters.module.css similarity index 100% rename from src/views/catalog/catalog-filters/catalog-filters.module.css rename to src/components/catalog-filters/catalog-filters.module.css diff --git a/src/views/catalog/catalog-filters/catalog-filters.tsx b/src/components/catalog-filters/catalog-filters.tsx similarity index 96% rename from src/views/catalog/catalog-filters/catalog-filters.tsx rename to src/components/catalog-filters/catalog-filters.tsx index fc403549..188ec343 100644 --- a/src/views/catalog/catalog-filters/catalog-filters.tsx +++ b/src/components/catalog-filters/catalog-filters.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store'; +import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import styles from './catalog-filters.module.css'; const FiltersTable = ({ onSave }: { onSave: () => void }) => { @@ -41,6 +41,8 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => { enabled: true, count: 0, filteredCids: new Set(), + subplebbitCounts: new Map(), + subplebbitFilteredCids: new Map>(), hide: true, top: false, }, diff --git a/src/views/catalog/catalog-filters/index.ts b/src/components/catalog-filters/index.ts similarity index 100% rename from src/views/catalog/catalog-filters/index.ts rename to src/components/catalog-filters/index.ts diff --git a/src/components/catalog-search/catalog-search.module.css b/src/components/catalog-search/catalog-search.module.css new file mode 100644 index 00000000..6c492561 --- /dev/null +++ b/src/components/catalog-search/catalog-search.module.css @@ -0,0 +1,49 @@ +.button { + text-transform: capitalize; + color: var(--button-desktop-text-color); + text-decoration: var(--button-text-decoration); +} + +.button:hover { + color: var(--button-desktop-text-color-hover); + cursor: pointer; +} + +.filtersButton { + text-transform: capitalize; +} + +.searchContainer { + display: inline; +} + +.searchContainer input { + width: 75px; + margin: 0px 5px; + padding: 1px; + outline: none; + border: 1px solid #AAA; + font-size: 11px; +} + +.searchContainer input:focus { + border: var(--post-form-field-input-focus-border); +} + +.closeSearch { + cursor: pointer; + border: none; + white-space: nowrap; + color: var(--button-desktop-text-color); +} + +.closeSearch:hover { + color: var(--button-desktop-text-color-hover); +} + +@media (max-width: 640px) { + .searchContainer input { + width: 75px; + font-size: 16px; + } +} diff --git a/src/components/catalog-search/catalog-search.tsx b/src/components/catalog-search/catalog-search.tsx new file mode 100644 index 00000000..7c723dfe --- /dev/null +++ b/src/components/catalog-search/catalog-search.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './catalog-search.module.css'; +import useIsMobile from '../../hooks/use-is-mobile'; + +const CatalogSearch = () => { + const { t } = useTranslation(); + const [openSearch, setOpenSearch] = useState(false); + + useEffect(() => { + if (openSearch) { + const input = document.querySelector('input'); + input?.focus(); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpenSearch(false); + } + }; + window.addEventListener('keydown', handleKeyDown); + } + }, [openSearch]); + + const isMobile = useIsMobile(); + + return ( + <> + {!isMobile && '['} + setOpenSearch(!openSearch)}> + {t('search')} + + {!isMobile && ']'} + {openSearch && ( +
+ + setOpenSearch(false)}> + ✖ + +
+ )} + + ); +}; + +export default CatalogSearch; diff --git a/src/components/catalog-search/index.ts b/src/components/catalog-search/index.ts new file mode 100644 index 00000000..aa2ace2c --- /dev/null +++ b/src/components/catalog-search/index.ts @@ -0,0 +1 @@ +export { default } from './catalog-search'; diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css index 7dca9193..649c43c1 100644 --- a/src/components/post-form/post-form.module.css +++ b/src/components/post-form/post-form.module.css @@ -15,8 +15,7 @@ .postFormMobile { text-align: center; - margin-top: -2px; - padding-bottom: 7px; + margin-top: -3px; } .closed { diff --git a/src/stores/use-catalog-filters-store.ts b/src/stores/use-catalog-filters-store.ts index 0d05f31b..1b7d9728 100644 --- a/src/stores/use-catalog-filters-store.ts +++ b/src/stores/use-catalog-filters-store.ts @@ -7,6 +7,8 @@ interface FilterItem { enabled: boolean; count: number; filteredCids: Set; + subplebbitCounts: Map; + subplebbitFilteredCids: Map>; hide: boolean; top: boolean; } @@ -24,8 +26,11 @@ interface CatalogFiltersStore { initializeFilter: () => void; filteredCount: number; filteredCids: Set; - incrementFilterCount: (filterIndex: number, cid: string) => void; + incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => void; recalcFilteredCount: () => void; + currentSubplebbitAddress: string | null; + setCurrentSubplebbitAddress: (address: string | null) => void; + getFilteredCountForCurrentSubplebbit: () => number; } const useCatalogFiltersStore = create( @@ -41,6 +46,8 @@ const useCatalogFiltersStore = create( filterItems: [], filteredCount: 0, filteredCids: new Set(), + currentSubplebbitAddress: null, + setCurrentSubplebbitAddress: (address: string | null) => set({ currentSubplebbitAddress: address }), setFilterItems: (items: FilterItem[]) => { const nonEmptyItems = items .filter((item) => item.text.trim() !== '') @@ -48,6 +55,8 @@ const useCatalogFiltersStore = create( ...item, count: item.count || 0, filteredCids: item.filteredCids || new Set(), + subplebbitCounts: item.subplebbitCounts || new Map(), + subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(), hide: item.hide ?? true, top: item.top ?? false, })); @@ -61,6 +70,8 @@ const useCatalogFiltersStore = create( ...item, count: item.count || 0, filteredCids: item.filteredCids || new Set(), + subplebbitCounts: item.subplebbitCounts || new Map(), + subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(), hide: item.hide ?? true, top: item.top ?? false, })); @@ -98,30 +109,75 @@ const useCatalogFiltersStore = create( initializeFilter: () => { get().updateFilter(); }, - incrementFilterCount: (filterIndex: number, cid: string) => { + incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => { set((state) => { const newFilterItems = [...state.filterItems]; if (newFilterItems[filterIndex]) { const item = newFilterItems[filterIndex]; - if (!item.filteredCids.has(cid)) { + + const subplebbitFilteredCids = new Map(item.subplebbitFilteredCids); + if (!subplebbitFilteredCids.has(subplebbitAddress)) { + subplebbitFilteredCids.set(subplebbitAddress, new Set()); + } + const cidSet = subplebbitFilteredCids.get(subplebbitAddress)!; + + if (!cidSet.has(cid)) { const newItemFilteredCids = new Set(item.filteredCids); newItemFilteredCids.add(cid); + + cidSet.add(cid); + subplebbitFilteredCids.set(subplebbitAddress, cidSet); + + const subplebbitCounts = new Map(item.subplebbitCounts); + const currentCount = subplebbitCounts.get(subplebbitAddress) || 0; + subplebbitCounts.set(subplebbitAddress, currentCount + 1); + newFilterItems[filterIndex] = { ...item, count: item.count + 1, filteredCids: newItemFilteredCids, + subplebbitCounts, + subplebbitFilteredCids, }; - const newFilteredCount = newFilterItems.reduce((sum, filt) => (filt.hide ? sum + filt.count : sum), 0); - return { filterItems: newFilterItems, filteredCount: newFilteredCount }; + + return { filterItems: newFilterItems }; } } return state; }); + + get().recalcFilteredCount(); }, recalcFilteredCount: () => { - set((state) => ({ - filteredCount: state.filterItems.reduce((sum, item) => (item.hide ? sum + item.count : sum), 0), - })); + set((state) => { + const currentSubplebbit = state.currentSubplebbitAddress; + if (!currentSubplebbit) return { filteredCount: 0 }; + + let filteredCount = 0; + for (const item of state.filterItems) { + if (item.enabled && item.hide) { + const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0; + filteredCount += subCount; + } + } + + return { filteredCount }; + }); + }, + getFilteredCountForCurrentSubplebbit: () => { + const state = get(); + const currentSubplebbit = state.currentSubplebbitAddress; + if (!currentSubplebbit) return 0; + + let filteredCount = 0; + for (const item of state.filterItems) { + if (item.enabled && item.hide) { + const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0; + filteredCount += subCount; + } + } + + return filteredCount; }, }), { @@ -134,6 +190,8 @@ const useCatalogFiltersStore = create( enabled: item.enabled, hide: item.hide, top: item.top, + subplebbitCounts: Array.from(item.subplebbitCounts || new Map()), + subplebbitFilteredCids: Array.from((item.subplebbitFilteredCids || new Map()).entries()).map(([key, value]) => [key, Array.from(value)]), })), } as any; }, diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index 16f8da82..64b60d83 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -23,7 +23,8 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const createContentFilter = ( filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean }[], - onFilterMatch?: (filterIndex: number, cid: string) => void, + subplebbitAddress: string, + onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void, ) => { // Create a unique key based on the enabled filter items const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== ''); @@ -51,10 +52,10 @@ const createContentFilter = ( const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled); if (filterIndex !== -1) { if (onFilterMatch) { - onFilterMatch(filterIndex, comment.cid); + onFilterMatch(filterIndex, comment.cid, subplebbitAddress); } else { // Fallback to the store method if no callback provided - useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid); + useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, subplebbitAddress); } } @@ -91,10 +92,11 @@ const createImageFilter = (showTextOnlyThreads: boolean) => { const createCombinedFilter = ( showTextOnlyThreads: boolean, filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean }[], - onFilterMatch?: (filterIndex: number, cid: string) => void, + subplebbitAddress: string, + onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void, ) => { const imageFilter = createImageFilter(showTextOnlyThreads); - const contentFilter = createContentFilter(filterItems, onFilterMatch); + const contentFilter = createContentFilter(filterItems, subplebbitAddress, onFilterMatch); return { filter: (comment: Comment) => { @@ -152,17 +154,25 @@ const Catalog = () => { const { timeFilterSeconds, timeFilterName } = useTimeFilter(); const { sortType } = useSortingStore(); - // stable callback for filter matching - const handleFilterMatch = useCallback((filterIndex: number, cid: string) => { - useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid); + // Create a stable callback for filter matching + const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => { + useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid, subplebbitAddress); }, []); + // Set the current subplebbit address + useEffect(() => { + useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(subplebbitAddress || null); + return () => { + useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(null); + }; + }, [subplebbitAddress]); + const feedOptions = useMemo(() => { const options: any = { subplebbitAddresses, sortType, postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage, - filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), + filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch), }; if (isInAllView || isInSubscriptionsView) { @@ -170,7 +180,18 @@ const Catalog = () => { } return options; - }, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, showTextOnlyThreads, filterItems, handleFilterMatch]); + }, [ + subplebbitAddresses, + sortType, + isInAllView, + isInSubscriptionsView, + postsPerPage, + timeFilterSeconds, + showTextOnlyThreads, + filterItems, + subplebbitAddress, + handleFilterMatch, + ]); const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions); const { accountComments } = useAccountComments(); @@ -226,14 +247,14 @@ const Catalog = () => { subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 7, - filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), + filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch), }); const { feed: monthlyFeed } = useFeed({ subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 30, - filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch), + filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch), }); const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);