feat(catalog filters): add color highlighting of threads matching pattern

This commit is contained in:
Tom (plebeius.eth)
2025-03-07 17:43:09 +01:00
parent 6edbed8d16
commit 22b828b3e8
12 changed files with 495 additions and 160 deletions
@@ -113,41 +113,6 @@
cursor: pointer;
}
.filtersProtip {
text-transform: none;
text-align: left;
}
.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 {
padding: 5px;
text-align: left;
@@ -247,17 +212,3 @@
font-size: 12px !important;
text-transform: none;
}
.clickbox {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 10px;
display: block;
text-align: center;
background-color: #fff;
border: 1px solid #aaa;
text-decoration: none;
color: #000;
margin: auto;
}
@@ -2,6 +2,8 @@ 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 FiltersProtip from './filters-protip';
import HighlightColorPicker from './highlight-color-picker';
import styles from './catalog-filters.module.css';
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
@@ -14,6 +16,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
...item,
hide: item.hide ?? true,
top: item.top ?? false,
color: item.color ?? '',
})),
);
@@ -26,6 +29,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
...item,
hide: item.hide ?? true,
top: item.top ?? false,
color: item.color ?? '',
})),
);
}, [filterItems]);
@@ -47,6 +51,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
subplebbitFilteredCids: new Map<string, Set<string>>(),
hide: true,
top: false,
color: '',
},
];
});
@@ -138,7 +143,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
/>
</td>
<td>
<span className={styles.clickbox} />
<HighlightColorPicker item={item} index={index} updateLocalFilterItem={updateLocalFilterItem} localFilterItems={localFilterItems} />
</td>
<td>
<input type='checkbox' checked={item.hide} onChange={(e) => updateLocalFilterItem(index, { ...item, hide: e.target.checked })} />
@@ -173,97 +178,26 @@ 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>{'/^&gt;/'}</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);
useEffect(() => {
const onEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (showHelp) {
closeHelp();
} else {
closeModal();
}
}
};
document.addEventListener('keydown', onEscapeKey);
return () => document.removeEventListener('keydown', onEscapeKey);
}, [closeModal, showHelp]);
return (
<>
<div className={styles.overlay} onClick={showHelp ? closeHelp : closeModal} />
@@ -287,16 +221,6 @@ const CatalogFilters = () => {
setShowModal(false);
}, []);
useEffect(() => {
const onEscapeKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', onEscapeKey);
return () => document.removeEventListener('keydown', onEscapeKey);
}, [closeModal]);
return (
<>
<span className={`${styles.filtersButton} button`} onClick={() => setShowModal(true)}>
@@ -0,0 +1,34 @@
.filtersProtip {
text-transform: none;
text-align: left;
}
.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;
}
@@ -0,0 +1,89 @@
import styles from './filters-protip.module.css';
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>{'/^&gt;/'}</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>
);
};
export default FiltersProtip;
@@ -0,0 +1 @@
export { default } from './filters-protip';
@@ -0,0 +1,110 @@
.colorClickbox {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 10px;
display: block;
text-align: center;
background-color: #fff;
border: var(--reply-modal-field-input-border, revert);
text-decoration: none;
color: #000;
position: relative;
cursor: pointer;
}
.colorPickerModal {
padding: 4px;
z-index: 1000;
font-size: 14px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
background-color: var(--settings-modal-background-color);
}
.colorPickerModal table {
border-spacing: 1px;
margin-left: auto;
margin-right: auto;
}
.colorPickerModal table td {
padding: 2px;
text-align: center;
font-size: 10pt;
margin: 0;
}
.colorOption {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
margin: auto;
width: 16px;
height: 16px;
line-height: 16px;
font-size: 10px;
display: block;
text-align: center;
border: 1px solid #aaa;
text-decoration: none;
color: #000;
white-space: nowrap;
user-select: none;
cursor: pointer;
}
.middleTxt input[type="text"] {
width: 45px;
margin: 0 2px;
margin-right: 5px;
padding: 1px;
outline: none;
border: var(--reply-modal-field-input-border, revert);
font-size: 11px;
background-color: default;
vertical-align: top;
}
.middleTxt input[type="text"]:focus {
border: var(--reply-modal-field-input-border-focus, revert);
}
@media (max-width: 640px) {
.middleTxt input[type="text"] {
font-size: 16px !important;
}
}
.middleTxt .colorClickbox {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 10px;
text-align: center;
border: 1px solid #aaa;
text-decoration: none;
color: #000;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
margin: auto;
cursor: pointer;
float: right;
background-color: transparent !important;
}
.colorPickerModal tfoot .button {
color: var(--button-desktop-text-color);
cursor: pointer;
margin: 0 2px;
}
.colorPickerModal tfoot .button:hover {
color: var(--button-desktop-text-color-hover);
}
.colorPickerModal tfoot .button::before{
content: '[';
color: var(--body-font-color);
}
.colorPickerModal tfoot .button::after{
content: ']';
color: var(--body-font-color);
}
@@ -0,0 +1,145 @@
import { useState, useRef } from 'react';
import styles from './highlight-color-picker.module.css';
import { autoUpdate, FloatingFocusManager, FloatingPortal, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
interface ColorPickerProps {
item: any;
index: number;
updateLocalFilterItem: (index: number, item: any) => void;
localFilterItems: any[];
}
const HighlightColorPicker = ({ item, index, updateLocalFilterItem, localFilterItems }: ColorPickerProps) => {
const [isColorPickerOpen, setIsColorPickerOpen] = useState<boolean>(false);
const [customColor, setCustomColor] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
const { refs, floatingStyles, context } = useFloating({
placement: 'left-start',
open: isColorPickerOpen,
onOpenChange: setIsColorPickerOpen,
middleware: [offset(10), shift()],
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
const headingId = useId();
const colorOptions = ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00', '#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB', '#00A550', '#007FFF', '#AF0A0F', '#B5BD68'];
const colorRows = [colorOptions.slice(0, 4), colorOptions.slice(4, 8), colorOptions.slice(8, 12)];
const setItemColor = (color: string) => {
updateLocalFilterItem(index, { ...localFilterItems[index], color });
setIsColorPickerOpen(false);
};
const handleCustomColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value;
setCustomColor(newColor);
};
const applyCustomColor = () => {
if (customColor) {
setItemColor(customColor);
}
};
const clearColor = () => {
setItemColor('');
};
return (
<>
<span className={styles.colorClickbox} style={{ backgroundColor: item.color || '#fff' }} ref={refs.setReference} {...getReferenceProps()}>
{!item.color && ''}
</span>
{isColorPickerOpen && (
<FloatingPortal>
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 999,
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsColorPickerOpen(false);
}}
/>
<FloatingFocusManager context={context} modal={false} initialFocus={-1}>
<div
className={styles.colorPickerModal}
ref={refs.setFloating}
style={floatingStyles}
aria-labelledby={headingId}
{...getFloatingProps()}
onClick={(e) => e.stopPropagation()}
>
<table className={styles.colorGrid}>
<tbody>
{colorRows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((color, colorIndex) => (
<td key={colorIndex}>
<span className={styles.colorOption} style={{ backgroundColor: color }} onClick={() => setItemColor(color)} />
</td>
))}
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={4}>Custom</td>
</tr>
<tr>
<td className={styles.middleTxt} colSpan={4}>
<input type='text' ref={inputRef} value={customColor} onChange={handleCustomColorChange} />
<span
className={styles.colorPreview}
style={{
backgroundColor: customColor || '#fff',
display: 'inline-block',
width: '16px',
height: '16px',
border: '1px solid #aaa',
verticalAlign: 'middle',
marginLeft: '5px',
cursor: 'pointer',
}}
onClick={applyCustomColor}
/>
</td>
</tr>
<tr>
<td colSpan={4}>
<span className={styles.button} onClick={() => setIsColorPickerOpen(false)}>
<span className={styles.buttonText}>Close</span>
</span>
<span className={styles.button} onClick={clearColor}>
<span className={styles.buttonText}>Clear</span>
</span>
</td>
</tr>
</tfoot>
</table>
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</>
);
};
export default HighlightColorPicker;
@@ -0,0 +1 @@
export { default } from './highlight-color-picker';
@@ -13,6 +13,8 @@
padding: 10px 0 3px 0;
vertical-align: top;
display: inline-block;
box-sizing: border-box;
margin: 2px;
}
.postContent {
@@ -69,14 +71,14 @@
background-color: var(--media-thumbnail-background-color);
width: var(--width);
height: var(--height);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
display: inline-flex;
justify-content: center;
align-items: center;
clip-path: inset(-5px); /* direct media could change its dimensions after being published, so we need to clip the overflow. We can't use overflow: hidden because it will hide the box-shadow */
overflow: hidden;
}
.post img, .post video, .post audio {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
max-width: var(--maxWidth);
max-height: var(--maxHeight);
}
+25 -9
View File
@@ -2,34 +2,36 @@ import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Link, useLocation, useParams } from 'react-router-dom';
import { Comment } from '@plebbit/plebbit-react-hooks';
import { useFloating, offset, size, autoUpdate, Placement } from '@floating-ui/react';
import { Comment } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import { shouldShowSnow } from '../../lib/snow';
import { getHasThumbnail } from '../../lib/utils/media-utils';
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useEditCommentPrivileges from '../../hooks/use-author-privileges';
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
import useHide from '../../hooks/use-hide';
import useWindowWidth from '../../hooks/use-window-width';
import useReplies from '../../hooks/use-replies';
import { ContentPreview } from '../../views/home/popular-threads-box';
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
import styles from './catalog-row.module.css';
import _ from 'lodash';
import { ContentPreview } from '../../views/home/popular-threads-box';
import { useCommentMediaInfo } from '../../hooks/use-comment-media-info';
import { shouldShowSnow } from '../../lib/snow';
import useReplies from '../../hooks/use-replies';
interface CatalogPostMediaProps {
cid: string;
commentMediaInfo: any;
isOutOfFeed?: boolean;
linkWidth?: number;
linkHeight?: number;
}
export const CatalogPostMedia = ({ commentMediaInfo, isOutOfFeed, linkWidth, linkHeight }: CatalogPostMediaProps) => {
export const CatalogPostMedia = ({ cid, commentMediaInfo, isOutOfFeed, linkWidth, linkHeight }: CatalogPostMediaProps) => {
const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {};
const iframeThumbnail = patternThumbnailUrl || thumbnail;
const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined);
@@ -89,8 +91,16 @@ export const CatalogPostMedia = ({ commentMediaInfo, isOutOfFeed, linkWidth, lin
thumbnailComponent = <audio src={url} controls />;
}
const matchedFilterColor = useCatalogFiltersStore((state) => state.matchedFilters.get(cid || ''));
return (
<div className={hasError ? '' : styles.mediaWrapper} style={CSSProperties}>
<div
className={hasError ? '' : styles.mediaWrapper}
style={{
...CSSProperties,
...(matchedFilterColor ? { border: `3px solid ${matchedFilterColor}` } : {}),
}}
>
{!isLoaded && !hasError && type !== 'video' && type !== 'audio' && <span className={styles.loadingSkeleton} />}
{hasError ? <img className={styles.fileDeleted} src='assets/filedeleted-res.gif' alt='' /> : thumbnailComponent}
</div>
@@ -250,7 +260,13 @@ const CatalogPost = ({ post }: { post: Comment }) => {
{spoiler ? (
<img src='assets/spoiler.png' alt='' />
) : (
<CatalogPostMedia commentMediaInfo={commentMediaInfo} isOutOfFeed={isDescription || isRules} linkWidth={linkWidth} linkHeight={linkHeight} />
<CatalogPostMedia
cid={cid}
commentMediaInfo={commentMediaInfo}
isOutOfFeed={isDescription || isRules}
linkWidth={linkWidth}
linkHeight={linkHeight}
/>
)}
</div>
</Link>
+26
View File
@@ -12,6 +12,7 @@ interface FilterItem {
subplebbitFilteredCids: Map<string, Set<string>>;
hide: boolean;
top: boolean;
color?: string;
}
interface CatalogFiltersStore {
@@ -36,6 +37,9 @@ interface CatalogFiltersStore {
setSearchFilter: (text: string) => void;
clearSearchFilter: () => void;
resetCountsForCurrentSubplebbit: () => void;
matchedFilters: Map<string, string>;
setMatchedFilter: (cid: string, color: string) => void;
clearMatchedFilters: () => void;
}
const useCatalogFiltersStore = create(
@@ -52,6 +56,21 @@ const useCatalogFiltersStore = create(
filteredCount: 0,
filteredCids: new Set<string>(),
currentSubplebbitAddress: null,
matchedFilters: new Map<string, string>(),
setMatchedFilter: (cid: string, color: string) => {
set((state) => {
const newMatchedFilters = new Map(state.matchedFilters);
if (color) {
newMatchedFilters.set(cid, color);
} else {
newMatchedFilters.delete(cid);
}
return { matchedFilters: newMatchedFilters };
});
},
clearMatchedFilters: () => {
set({ matchedFilters: new Map<string, string>() });
},
setCurrentSubplebbitAddress: (address: string | null) => {
const prevAddress = get().currentSubplebbitAddress;
@@ -123,6 +142,7 @@ const useCatalogFiltersStore = create(
subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true,
top: item.top ?? false,
color: item.color || '',
}));
set({ filterItems: nonEmptyItems });
get().recalcFilteredCount();
@@ -138,6 +158,7 @@ const useCatalogFiltersStore = create(
subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true,
top: item.top ?? false,
color: item.color || '',
}));
// Compare new filter items with existing ones to detect pattern changes
@@ -154,6 +175,7 @@ const useCatalogFiltersStore = create(
filteredCids: existingItem.filteredCids,
subplebbitCounts: existingItem.subplebbitCounts,
subplebbitFilteredCids: existingItem.subplebbitFilteredCids,
color: newItem.color || '',
};
}
@@ -164,9 +186,13 @@ const useCatalogFiltersStore = create(
filteredCids: new Set<string>(),
subplebbitCounts: new Map<string, number>(),
subplebbitFilteredCids: new Map<string, Set<string>>(),
color: newItem.color || '',
};
});
// Clear matched filters when saving new filters
get().clearMatchedFilters();
set({
filterItems: updatedItems,
filteredCids: new Set<string>(),
+39 -3
View File
@@ -23,7 +23,7 @@ import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const createContentFilter = (
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
subplebbitAddress: string,
onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => {
@@ -55,6 +55,11 @@ const createContentFilter = (
// Fallback to the store method if no callback provided
useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, subplebbitAddress);
}
// If the filter has a color, track it in the matchedFilters map
if (item.color) {
useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color);
}
}
// If this filter is set to hide, filter out the comment
@@ -89,7 +94,7 @@ const createImageFilter = (showTextOnlyThreads: boolean) => {
const createCombinedFilter = (
showTextOnlyThreads: boolean,
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
searchText: string,
subplebbitAddress: string,
onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
@@ -125,7 +130,7 @@ const Catalog = () => {
const isInAllView = isAllView(location.pathname);
const defaultSubplebbits = useDefaultSubplebbits();
const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore();
const { showTextOnlyThreads, filterItems, searchText } = useCatalogFiltersStore();
const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
const account = useAccount();
const subscriptions = account?.subscriptions;
@@ -446,6 +451,37 @@ const Catalog = () => {
document.title = documentTitle + ` - ${t('catalog')} - plebchan`;
}, [title, shortAddress, isInAllView, isInSubscriptionsView, t]);
// Clear matched filters when component mounts or when subplebbit changes
useEffect(() => {
clearMatchedFilters();
return () => {
clearMatchedFilters();
};
}, [clearMatchedFilters, subplebbitAddress]);
// Apply filter colors to posts when feed changes
useEffect(() => {
if (combinedFeed.length > 0 && filterItems.length > 0) {
// Clear existing matched filters
clearMatchedFilters();
// Apply colors to posts that match filters
combinedFeed.forEach((comment) => {
if (!comment?.cid) return;
// Check each filter
for (const item of filterItems) {
if (item.enabled && item.text.trim() !== '' && item.color) {
if (commentMatchesPattern(comment, item.text)) {
useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color);
break; // Use the first matching filter's color
}
}
}
});
}
}, [combinedFeed, filterItems, clearMatchedFilters]);
return (
<div className={styles.content}>
<hr />