mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(catalog filter): add filtering with complex patterns, including regex, help modal
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
|
||||
.modal {
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
left: calc(50% + 5px);
|
||||
transform: translateX(-50%);
|
||||
width: 380px;
|
||||
height: auto;
|
||||
@@ -41,14 +41,23 @@
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
padding: 2px;
|
||||
padding: 2px 5px 5px 5px;
|
||||
font-size: var(--settings-modal-font-size);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
|
||||
background-color: var(--settings-modal-background-color);
|
||||
border-top: var(--settings-modal-border-top);
|
||||
border-right: var(--settings-modal-border-right);
|
||||
border-bottom: var(--settings-modal-border-bottom);
|
||||
border-left: var(--settings-modal-border-left);
|
||||
}
|
||||
|
||||
.filtersProtipModal {
|
||||
top: 50px;
|
||||
width: 570px;
|
||||
left: 50%;
|
||||
padding: 2px 10px 5px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 614px) {
|
||||
.filtersProtipModal {
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
@@ -82,6 +91,54 @@
|
||||
top: 5px;
|
||||
image-rendering: pixelated;
|
||||
background-image: var(--settings-modal-close-button-background-image);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.openHelpButton {
|
||||
right: 23px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
background-size: 100%;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
image-rendering: pixelated;
|
||||
background-image: var(--open-help-button-background-image);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filtersProtip {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.filtersProtip code {
|
||||
padding: 1px 5px 1px 5px;
|
||||
background-color: #EEE;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.filtersProtip li {
|
||||
list-style: none;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.filtersProtip ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filtersProtip h4 {
|
||||
font-size: 15px;
|
||||
margin: 10px 0px 5px 0px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.filtersProtip h4:first-child {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.filtersProtip h4::before {
|
||||
content: "»";
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
@@ -108,16 +165,12 @@
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.filtersTable {
|
||||
width: 100%;
|
||||
border-spacing: 1px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 10px 2px 2px 2px;
|
||||
padding: 0px 2px 2px 2px;
|
||||
}
|
||||
|
||||
.filtersTable th {
|
||||
@@ -168,7 +221,8 @@
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
float: right;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.filterHits {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import styles from './catalog-filters.module.css';
|
||||
|
||||
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { filterItems, saveAndApplyFilters, currentSubplebbitAddress } = useCatalogFiltersStore();
|
||||
const resetFeed = useFeedResetStore((state) => state.reset);
|
||||
|
||||
const [localFilterItems, setLocalFilterItems] = useState(
|
||||
filterItems.map((item) => ({
|
||||
@@ -52,9 +54,17 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
|
||||
|
||||
saveAndApplyFilters(nonEmptyFilters);
|
||||
|
||||
useCatalogFiltersStore.getState().resetCountsForCurrentSubplebbit();
|
||||
|
||||
if (resetFeed) {
|
||||
resetFeed();
|
||||
}
|
||||
|
||||
onSave();
|
||||
}, [saveAndApplyFilters, localFilterItems, onSave]);
|
||||
}, [saveAndApplyFilters, localFilterItems, onSave, resetFeed]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
@@ -142,7 +152,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.filterHits}>
|
||||
{currentSubplebbitAddress && (item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0) > 0 && `x${item.subplebbitCounts?.get(currentSubplebbitAddress)}`}
|
||||
{currentSubplebbitAddress && item.subplebbitFilteredCids?.has(currentSubplebbitAddress) && `x${item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0}`}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -163,18 +173,107 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const FiltersProtip = () => {
|
||||
return (
|
||||
<div className={styles.filtersProtip}>
|
||||
<h4>Patterns</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Matching whole words:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>feel</code> — will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>AND operator:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>feel girlfriend</code> — will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>OR operator:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>feel|girlfriend</code> — will match <em>"feel"</em> OR <em>"girlfriend"</em>.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Mixing both operators:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>girlfriend|boyfriend feel</code> — matches <em>"feel"</em> AND <em>"girlfriend"</em>, or <em>"feel"</em> AND <em>"boyfriend"</em>.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Exact match search:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>"that feel when"</code> — place double quotes around the pattern to search for an exact string.
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Wildcards:</strong>
|
||||
</li>
|
||||
<li>
|
||||
<code>feel*</code> — matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etc…
|
||||
</li>
|
||||
<li>
|
||||
<code>idolm*ster</code> — this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etc…
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<strong>It is also possible to filter by regular expression:</strong>
|
||||
<li>
|
||||
<code>/^(?=.*detachable)(?=.*hats).*$/i</code> — AND operator.
|
||||
</li>
|
||||
<li>
|
||||
<code>/^(?!.*touhou).*$/i</code> — NOT operator.
|
||||
</li>
|
||||
<li>
|
||||
<code>{'/^>/'}</code> — threads starting with a quote (<em>{'">"'}</em> character as an html entity).
|
||||
</li>
|
||||
<li>
|
||||
<code>/^$/</code> — threads with no text.
|
||||
</li>
|
||||
</ul>
|
||||
<h4>Controls</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>On</strong> — enables or disables the filter.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Hide</strong> — hides matched threads.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Top</strong> — moves the filter to the top of the feed.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const openHelp = () => setShowHelp(true);
|
||||
const closeHelp = () => setShowHelp(false);
|
||||
return (
|
||||
<>
|
||||
<div className={styles.overlay} onClick={closeModal} />
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.overlay} onClick={showHelp ? closeHelp : closeModal} />
|
||||
<div className={`${styles.modal} ${showHelp ? styles.filtersProtipModal : ''}`}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{t('filters_and_highlights')}</span>
|
||||
<span className={styles.closeButton} title='close' onClick={closeModal} />
|
||||
<span className={styles.title}>{showHelp ? t('filter_and_highlights_help') : t('filter_and_highlights')}</span>
|
||||
{!showHelp && <span className={styles.openHelpButton} title={t('help')} onClick={openHelp} />}
|
||||
<span className={styles.closeButton} title={t('close')} onClick={closeModal} />
|
||||
</div>
|
||||
<FiltersTable onSave={closeModal} />
|
||||
{showHelp ? <FiltersProtip /> : <FiltersTable onSave={closeModal} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
|
||||
/**
|
||||
* Checks if a text matches a pattern according to various pattern matching rules:
|
||||
* - Whole word matching: 'feel' matches 'feel' but not 'feeling'
|
||||
* - AND operator: 'feel girlfriend' matches both words in any order
|
||||
* - OR operator: 'feel|girlfriend' matches either word
|
||||
* - Mixed operators: 'girlfriend|boyfriend feel' matches complex logic
|
||||
* - Exact match: "that feel when" matches the exact phrase
|
||||
* - Wildcards: 'feel*' matches 'feel', 'feels', 'feeling', etc.
|
||||
* - Regex: /pattern/flags for regular expressions
|
||||
*
|
||||
* @param text The text to check against the pattern
|
||||
* @param pattern The pattern to match
|
||||
* @returns True if the text matches the pattern, false otherwise
|
||||
*/
|
||||
export const matchesPattern = (text: string, pattern: string): boolean => {
|
||||
if (!pattern || !text) return false;
|
||||
|
||||
const textLower = text.toLowerCase();
|
||||
|
||||
try {
|
||||
// Check if it's a regex pattern (starts and ends with /)
|
||||
if (pattern.startsWith('/') && pattern.length > 2 && /\/[gimsuy]*$/.test(pattern)) {
|
||||
const lastSlashIndex = pattern.lastIndexOf('/');
|
||||
const regexPattern = pattern.substring(1, lastSlashIndex);
|
||||
const flags = pattern.substring(lastSlashIndex + 1);
|
||||
const regex = new RegExp(regexPattern, flags);
|
||||
return regex.test(textLower);
|
||||
}
|
||||
// Check if it's an exact match pattern (surrounded by quotes)
|
||||
else if (pattern.startsWith('"') && pattern.endsWith('"') && pattern.length > 2) {
|
||||
const exactPattern = pattern.substring(1, pattern.length - 1).toLowerCase();
|
||||
return textLower.includes(exactPattern);
|
||||
}
|
||||
// Check for OR patterns (contains |)
|
||||
else if (pattern.includes('|')) {
|
||||
const orGroups = pattern.split(' ').filter(Boolean);
|
||||
|
||||
// Process each group (which might contain OR operators)
|
||||
return orGroups.every((group) => {
|
||||
const orTerms = group.split('|').map((term) => term.toLowerCase());
|
||||
return orTerms.some((term) => {
|
||||
// Handle wildcards in OR terms
|
||||
if (term.includes('*')) {
|
||||
const regexPattern = term.replace(/\*/g, '.*').toLowerCase();
|
||||
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
} else {
|
||||
// Match whole word only
|
||||
const regex = new RegExp(`\\b${term}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Check for AND patterns (space-separated words)
|
||||
else if (pattern.includes(' ')) {
|
||||
const andTerms = pattern
|
||||
.split(' ')
|
||||
.filter(Boolean)
|
||||
.map((term) => term.toLowerCase());
|
||||
return andTerms.every((term) => {
|
||||
// Handle wildcards in AND terms
|
||||
if (term.includes('*')) {
|
||||
const regexPattern = term.replace(/\*/g, '.*').toLowerCase();
|
||||
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
} else {
|
||||
// Match whole word only
|
||||
const regex = new RegExp(`\\b${term}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle wildcard patterns
|
||||
else if (pattern.includes('*')) {
|
||||
const regexPattern = pattern.replace(/\*/g, '.*').toLowerCase();
|
||||
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
}
|
||||
// Simple whole word match
|
||||
else {
|
||||
const regex = new RegExp(`\\b${pattern.toLowerCase()}\\b`, 'i');
|
||||
return regex.test(textLower);
|
||||
}
|
||||
} catch (error) {
|
||||
// If regex parsing fails, fall back to simple includes
|
||||
console.error('Pattern matching error:', error);
|
||||
const simplifiedPattern = pattern.toLowerCase();
|
||||
return textLower.includes(simplifiedPattern);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a comment matches a pattern in its title or content
|
||||
*
|
||||
* @param comment The comment to check
|
||||
* @param pattern The pattern to match
|
||||
* @returns True if the comment matches the pattern, false otherwise
|
||||
*/
|
||||
export const commentMatchesPattern = (comment: Comment, pattern: string): boolean => {
|
||||
if (!pattern || !comment) return false;
|
||||
|
||||
const titleLower = comment?.title?.toLowerCase() || '';
|
||||
const contentLower = comment?.content?.toLowerCase() || '';
|
||||
const textToMatch = titleLower + ' ' + contentLower;
|
||||
|
||||
return matchesPattern(textToMatch, pattern);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { commentMatchesPattern } from '../lib/utils/pattern-utils';
|
||||
|
||||
interface FilterItem {
|
||||
text: string;
|
||||
@@ -34,6 +35,7 @@ interface CatalogFiltersStore {
|
||||
searchText: string;
|
||||
setSearchFilter: (text: string) => void;
|
||||
clearSearchFilter: () => void;
|
||||
resetCountsForCurrentSubplebbit: () => void;
|
||||
}
|
||||
|
||||
const useCatalogFiltersStore = create(
|
||||
@@ -137,10 +139,39 @@ const useCatalogFiltersStore = create(
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
}));
|
||||
|
||||
// Compare new filter items with existing ones to detect pattern changes
|
||||
const existingItems = get().filterItems;
|
||||
const updatedItems = nonEmptyItems.map((newItem) => {
|
||||
// Try to find matching existing item by index or text
|
||||
const existingItem = existingItems.find((item) => item.text === newItem.text);
|
||||
|
||||
// If we found a matching item, keep its counts
|
||||
if (existingItem) {
|
||||
return {
|
||||
...newItem,
|
||||
count: existingItem.count,
|
||||
filteredCids: existingItem.filteredCids,
|
||||
subplebbitCounts: existingItem.subplebbitCounts,
|
||||
subplebbitFilteredCids: existingItem.subplebbitFilteredCids,
|
||||
};
|
||||
}
|
||||
|
||||
// If pattern has changed or it's a new filter, reset all counts
|
||||
return {
|
||||
...newItem,
|
||||
count: 0,
|
||||
filteredCids: new Set<string>(),
|
||||
subplebbitCounts: new Map<string, number>(),
|
||||
subplebbitFilteredCids: new Map<string, Set<string>>(),
|
||||
};
|
||||
});
|
||||
|
||||
set({
|
||||
filterItems: nonEmptyItems,
|
||||
filterItems: updatedItems,
|
||||
filteredCids: new Set<string>(),
|
||||
});
|
||||
|
||||
get().recalcFilteredCount();
|
||||
get().updateFilter();
|
||||
},
|
||||
@@ -154,11 +185,7 @@ const useCatalogFiltersStore = create(
|
||||
|
||||
// Apply search filter
|
||||
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)) {
|
||||
if (!commentMatchesPattern(comment, state.searchText)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -170,11 +197,7 @@ const useCatalogFiltersStore = create(
|
||||
for (let i = 0; i < filterItems.length; i++) {
|
||||
const item = filterItems[i];
|
||||
if (item.enabled && item.text.trim() !== '') {
|
||||
const pattern = item.text.toLowerCase();
|
||||
const title = comment?.title?.toLowerCase() || '';
|
||||
const content = comment?.content?.toLowerCase() || '';
|
||||
|
||||
if (title.includes(pattern) || content.includes(pattern)) {
|
||||
if (commentMatchesPattern(comment, item.text)) {
|
||||
// If we have a current subplebbit and this is a match, increment the count
|
||||
if (currentSubplebbit && comment.subplebbitAddress === currentSubplebbit) {
|
||||
// We need to use a timeout to avoid modifying state during a state update
|
||||
@@ -272,6 +295,38 @@ const useCatalogFiltersStore = create(
|
||||
|
||||
return filteredCount;
|
||||
},
|
||||
resetCountsForCurrentSubplebbit: () => {
|
||||
const currentSubplebbit = get().currentSubplebbitAddress;
|
||||
if (!currentSubplebbit) return;
|
||||
|
||||
set((state) => {
|
||||
const updatedFilterItems = state.filterItems.map((item) => {
|
||||
const newItem = { ...item };
|
||||
|
||||
// Ensure maps are properly initialized
|
||||
if (!newItem.subplebbitCounts || !(newItem.subplebbitCounts instanceof Map)) {
|
||||
newItem.subplebbitCounts = new Map();
|
||||
}
|
||||
if (!newItem.subplebbitFilteredCids || !(newItem.subplebbitFilteredCids instanceof Map)) {
|
||||
newItem.subplebbitFilteredCids = new Map();
|
||||
}
|
||||
|
||||
// Reset counts for current subplebbit
|
||||
newItem.subplebbitCounts.set(currentSubplebbit, 0);
|
||||
newItem.subplebbitFilteredCids.set(currentSubplebbit, new Set<string>());
|
||||
|
||||
return newItem;
|
||||
});
|
||||
|
||||
return {
|
||||
filterItems: updatedFilterItems,
|
||||
filteredCount: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// Trigger filter reapplication to start counting again
|
||||
get().updateFilter();
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'catalog-filters-storage',
|
||||
|
||||
+6
-3
@@ -50,6 +50,7 @@
|
||||
--button-desktop-text-color: #00e;
|
||||
--button-desktop-text-color-hover: red;
|
||||
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
--open-help-button-background-image: url("/assets/buttons/help-red.png");
|
||||
|
||||
/* horizontal rule */
|
||||
--hr-border: none;
|
||||
@@ -263,6 +264,7 @@
|
||||
--button-desktop-text-color: #34345c;
|
||||
--button-desktop-text-color-hover: #d00;
|
||||
--close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
--open-help-button-background-image: url("/assets/buttons/help-blue.png");
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
@@ -479,6 +481,7 @@
|
||||
--button-desktop-text-color-hover: red;
|
||||
--button-text-decoration: underline;
|
||||
--close-button-background-image: url("/assets/buttons/cross-red.png");
|
||||
--open-help-button-background-image: url("/assets/buttons/help-red.png");
|
||||
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #fed;
|
||||
@@ -657,7 +660,7 @@
|
||||
--button-text-decoration: underline;
|
||||
--button-desktop-text-color-hover: #d00;
|
||||
--close-button-background-image: url("/assets/buttons/cross-blue.png");
|
||||
|
||||
--open-help-button-background-image: url("/assets/buttons/help-blue.png");
|
||||
/* footer */
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
|
||||
@@ -848,7 +851,7 @@
|
||||
--button-desktop-text-color: #81a2be;
|
||||
--button-desktop-text-color-hover: #5f89ac;
|
||||
--close-button-background-image: url("/assets/buttons/cross-dark.png");
|
||||
|
||||
--open-help-button-background-image: url("/assets/buttons/help-dark.png");
|
||||
/* filter for inputs */
|
||||
--filter80: brightness(85%);
|
||||
|
||||
@@ -1067,7 +1070,7 @@
|
||||
--button-desktop-text-color: #f60;
|
||||
--button-desktop-text-color-hover: #f30;
|
||||
--close-button-background-image: url("/assets/buttons/cross-photon.png");
|
||||
|
||||
--open-help-button-background-image: url("/assets/buttons/help-photon.png");
|
||||
/* horizontal rule */
|
||||
--hr-border: none;
|
||||
--hr-border-top: 1px solid #ddd;
|
||||
|
||||
@@ -18,6 +18,7 @@ import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import CatalogRow from '../../components/catalog-row';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import styles from './catalog.module.css';
|
||||
import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
@@ -39,15 +40,12 @@ const createContentFilter = (
|
||||
|
||||
if (enabledFilters.length === 0) return true;
|
||||
|
||||
const titleLower = comment?.title?.toLowerCase() || '';
|
||||
const contentLower = comment?.content?.toLowerCase() || '';
|
||||
|
||||
// Check if any enabled filter matches the content
|
||||
for (let i = 0; i < enabledFilters.length; i++) {
|
||||
const item = enabledFilters[i];
|
||||
const pattern = item.text.toLowerCase();
|
||||
const pattern = item.text;
|
||||
|
||||
if (titleLower.includes(pattern) || contentLower.includes(pattern)) {
|
||||
if (commentMatchesPattern(comment, pattern)) {
|
||||
// Find the original filter index to increment count
|
||||
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
|
||||
if (filterIndex !== -1) {
|
||||
@@ -102,12 +100,7 @@ const createCombinedFilter = (
|
||||
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);
|
||||
return commentMatchesPattern(comment, searchText);
|
||||
},
|
||||
key: searchText ? `search-filter-${searchText}` : 'no-search-filter',
|
||||
};
|
||||
@@ -410,13 +403,9 @@ const Catalog = () => {
|
||||
combinedFeed.forEach((comment) => {
|
||||
if (!comment) return;
|
||||
|
||||
const titleLower = comment?.title?.toLowerCase() || '';
|
||||
const contentLower = comment?.content?.toLowerCase() || '';
|
||||
|
||||
let isTop = false;
|
||||
for (const filter of enabledTopFilters) {
|
||||
const pattern = filter.text.toLowerCase();
|
||||
if (titleLower.includes(pattern) || contentLower.includes(pattern)) {
|
||||
if (commentMatchesPattern(comment, filter.text)) {
|
||||
isTop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user