feat(catalog): add search

This commit is contained in:
Tom (plebeius.eth)
2025-03-06 16:27:09 +01:00
parent cf183393a4
commit a8f7de7d85
41 changed files with 213 additions and 60 deletions
+22 -6
View File
@@ -246,7 +246,7 @@ export const MobileBoardButtons = () => {
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
const { filteredCount } = useCatalogFiltersStore();
const { filteredCount, searchText } = useCatalogFiltersStore();
return (
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
@@ -269,11 +269,19 @@ export const MobileBoardButtons = () => {
)}
{!(isInAllView || isInSubscriptionsView) && <SubscribeButton address={subplebbitAddress} />}
<RefreshButton />
{isInCatalogView && filteredCount > 0 && (
{isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}>
{' '}
{t('filtered_threads')}: <strong>{filteredCount}</strong>
{t('search_results_for')}: <strong>{searchText}</strong>
</span>
) : (
isInCatalogView &&
filteredCount > 0 && (
<span className={styles.filteredThreadsCount}>
{' '}
{t('filtered_threads')}: <strong>{filteredCount}</strong>
</span>
)
)}
{isInCatalogView && (
<>
@@ -331,7 +339,7 @@ export const DesktopBoardButtons = () => {
const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const { filteredCount } = useCatalogFiltersStore();
const { filteredCount, searchText } = useCatalogFiltersStore();
return (
<>
@@ -364,11 +372,19 @@ export const DesktopBoardButtons = () => {
</>
)}
[<RefreshButton />]
{isInCatalogView && filteredCount > 0 && (
{isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}>
{' '}
{t('filtered_threads')}: <strong>{filteredCount}</strong>
{t('search_results_for')}: <strong>{searchText}</strong>
</span>
) : (
isInCatalogView &&
filteredCount > 0 && (
<span className={styles.filteredThreadsCount}>
{' '}
{t('filtered_threads')}: <strong>{filteredCount}</strong>
</span>
)
)}
<span className={styles.rightSideButtons}>
{isInCatalogView && (
@@ -1,11 +1,43 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './catalog-search.module.css';
import useIsMobile from '../../hooks/use-is-mobile';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import _ from 'lodash';
const CatalogSearch = () => {
const { t } = useTranslation();
const [openSearch, setOpenSearch] = useState(false);
const [inputValue, setInputValue] = useState('');
const { setSearchFilter, clearSearchFilter } = useCatalogFiltersStore();
// Create a debounced version of setSearchFilter
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedSetSearchFilter = useCallback(
_.debounce((text: string) => {
if (text.trim()) {
setSearchFilter(text);
} else {
clearSearchFilter();
}
}, 300),
[setSearchFilter, clearSearchFilter],
);
const handleToggleSearch = useCallback(() => {
setOpenSearch((prev) => !prev);
if (openSearch) {
setInputValue('');
clearSearchFilter();
}
}, [openSearch, clearSearchFilter]);
const handleCloseSearch = useCallback(() => {
setOpenSearch(false);
setInputValue('');
clearSearchFilter();
}, [clearSearchFilter]);
useEffect(() => {
if (openSearch) {
@@ -14,26 +46,42 @@ const CatalogSearch = () => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOpenSearch(false);
handleCloseSearch();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}
}, [openSearch]);
}, [openSearch, handleCloseSearch]);
useEffect(() => {
debouncedSetSearchFilter(inputValue);
return () => {
debouncedSetSearchFilter.cancel();
};
}, [inputValue, debouncedSetSearchFilter]);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const isMobile = useIsMobile();
return (
<>
{!isMobile && '['}
<span className={`${styles.filtersButton} button`} onClick={() => setOpenSearch(!openSearch)}>
<span className={`${styles.filtersButton} button`} onClick={handleToggleSearch}>
{t('search')}
</span>
{!isMobile && ']'}
{openSearch && (
<div className={styles.searchContainer}>
<input type='text' />
<span className={styles.closeSearch} onClick={() => setOpenSearch(false)}>
<input type='text' value={inputValue} onChange={handleSearchChange} />
<span className={styles.closeSearch} onClick={handleCloseSearch}>
</span>
</div>
+4 -2
View File
@@ -7,6 +7,7 @@ import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
import { isAllView } from '../lib/utils/view-utils';
import { useMultisubMetadata } from './use-default-subplebbits';
import _ from 'lodash';
import useCatalogFiltersStore from '../stores/use-catalog-filters-store';
const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subplebbit: Subplebbit) => {
const { t } = useTranslation();
@@ -19,6 +20,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
const multisub = useMultisubMetadata();
const { accountComments } = useAccountComments();
const { searchText } = useCatalogFiltersStore();
const feedWithFakePostsOnTop = useMemo(() => {
if (!isFeedLoaded) {
@@ -64,7 +66,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
}
// add subplebbit description and rules as fake posts at the top of the feed
if (description && description.length > 0) {
if (description && description.length > 0 && !searchText) {
_feed.unshift({
isDescription: true,
subplebbitAddress: address,
@@ -79,7 +81,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
}
// rules are shown in description thread if both are set
if (rules && rules.length > 0 && !description) {
if (rules && rules.length > 0 && !description && !searchText) {
_feed.unshift({
isRules: true,
subplebbitAddress: address,
+23
View File
@@ -31,6 +31,9 @@ interface CatalogFiltersStore {
currentSubplebbitAddress: string | null;
setCurrentSubplebbitAddress: (address: string | null) => void;
getFilteredCountForCurrentSubplebbit: () => number;
searchText: string;
setSearchFilter: (text: string) => void;
clearSearchFilter: () => void;
}
const useCatalogFiltersStore = create(
@@ -48,6 +51,15 @@ const useCatalogFiltersStore = create(
filteredCids: new Set<string>(),
currentSubplebbitAddress: null,
setCurrentSubplebbitAddress: (address: string | null) => set({ currentSubplebbitAddress: address }),
searchText: '',
setSearchFilter: (text: string) => {
set({ searchText: text });
get().updateFilter();
},
clearSearchFilter: () => {
set({ searchText: '' });
get().updateFilter();
},
setFilterItems: (items: FilterItem[]) => {
const nonEmptyItems = items
.filter((item) => item.text.trim() !== '')
@@ -87,6 +99,17 @@ const useCatalogFiltersStore = create(
set((state) => ({
filter: (comment: Comment) => {
if (!comment?.cid) return true;
if (state.searchText.trim() !== '') {
const searchPattern = state.searchText.toLowerCase();
const title = comment?.title?.toLowerCase() || '';
const content = comment?.content?.toLowerCase() || '';
if (!title.includes(searchPattern) && !content.includes(searchPattern)) {
return false;
}
}
const { filterItems } = state;
let shouldHide = false;
for (let i = 0; i < filterItems.length; i++) {
+39 -10
View File
@@ -92,19 +92,35 @@ const createImageFilter = (showTextOnlyThreads: boolean) => {
const createCombinedFilter = (
showTextOnlyThreads: boolean,
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
searchText: string,
subplebbitAddress: string,
onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => {
const imageFilter = createImageFilter(showTextOnlyThreads);
const contentFilter = createContentFilter(filterItems, subplebbitAddress, onFilterMatch);
const searchFilter = {
filter: (comment: Comment) => {
if (!searchText.trim()) return true;
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
const searchPattern = searchText.toLowerCase();
return titleLower.includes(searchPattern) || contentLower.includes(searchPattern);
},
key: searchText ? `search-filter-${searchText}` : 'no-search-filter',
};
return {
filter: (comment: Comment) => {
if (!imageFilter.filter(comment)) return false;
if (!contentFilter.filter(comment)) return false;
if (!searchFilter.filter(comment)) return false;
return contentFilter.filter(comment);
return true;
},
key: `${imageFilter.key}-${contentFilter.key}`,
key: `${imageFilter.key}-${contentFilter.key}-${searchFilter.key}`,
};
};
@@ -116,7 +132,7 @@ const Catalog = () => {
const isInAllView = isAllView(location.pathname);
const defaultSubplebbits = useDefaultSubplebbits();
const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore();
const { showTextOnlyThreads, filterItems } = useCatalogFiltersStore();
const { showTextOnlyThreads, filterItems, searchText } = useCatalogFiltersStore();
const account = useAccount();
const subscriptions = account?.subscriptions;
@@ -172,7 +188,7 @@ const Catalog = () => {
subplebbitAddresses,
sortType,
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
};
if (isInAllView || isInSubscriptionsView) {
@@ -189,6 +205,7 @@ const Catalog = () => {
timeFilterSeconds,
showTextOnlyThreads,
filterItems,
searchText,
subplebbitAddress,
handleFilterMatch,
]);
@@ -203,7 +220,9 @@ const Catalog = () => {
() =>
accountComments.filter((comment) => {
const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {};
return (
// Basic filtering conditions
const basicConditions =
!deleted &&
!removed &&
timestamp > Date.now() / 1000 - 60 * 60 &&
@@ -212,10 +231,20 @@ const Catalog = () => {
(showTextOnlyThreads ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
cid === postCid &&
comment?.subplebbitAddress === subplebbitAddress &&
!feed.some((post) => post.cid === cid)
);
!feed.some((post) => post.cid === cid);
// If search is active, also check search conditions
if (basicConditions && searchText.trim()) {
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
const searchPattern = searchText.toLowerCase();
return titleLower.includes(searchPattern) || contentLower.includes(searchPattern);
}
return basicConditions;
}),
[accountComments, subplebbitAddress, feed, showTextOnlyThreads],
[accountComments, subplebbitAddress, feed, showTextOnlyThreads, searchText],
);
// show newest account comment at the top of the feed but after pinned posts
@@ -247,14 +276,14 @@ const Catalog = () => {
subplebbitAddresses,
sortType,
newerThan: 60 * 60 * 24 * 7,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
});
const { feed: monthlyFeed } = useFeed({
subplebbitAddresses,
sortType,
newerThan: 60 * 60 * 24 * 30,
filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
});
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
+1 -1
View File
@@ -562,7 +562,7 @@
width: 24px;
max-height: 24px;
position: absolute;
bottom: -5px;
bottom: -7px;
}
@media (max-width: 640px) {