mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(catalog): add catalog filters
This commit is contained in:
@@ -13,7 +13,6 @@ 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 { useEffect } from 'react';
|
||||
|
||||
interface BoardButtonsProps {
|
||||
address?: string | undefined;
|
||||
@@ -244,13 +243,7 @@ export const MobileBoardButtons = () => {
|
||||
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
|
||||
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
|
||||
|
||||
const { filteredCount, resetFilteredCount } = useCatalogFiltersStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (subplebbitAddress) {
|
||||
resetFilteredCount();
|
||||
}
|
||||
}, [subplebbitAddress, resetFilteredCount]);
|
||||
const { filteredCount } = useCatalogFiltersStore();
|
||||
|
||||
return (
|
||||
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
|
||||
@@ -334,13 +327,7 @@ export const DesktopBoardButtons = () => {
|
||||
const isInPostView = isPostPageView(location.pathname, params);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
|
||||
|
||||
const { filteredCount, resetFilteredCount } = useCatalogFiltersStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (subplebbitAddress) {
|
||||
resetFilteredCount();
|
||||
}
|
||||
}, [subplebbitAddress, resetFilteredCount]);
|
||||
const { filteredCount } = useCatalogFiltersStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
|
||||
|
||||
interface FilterItem {
|
||||
text: string;
|
||||
enabled: boolean;
|
||||
count: number;
|
||||
filteredCids: Set<string>;
|
||||
hide: boolean;
|
||||
top: boolean;
|
||||
}
|
||||
|
||||
interface CatalogFiltersStore {
|
||||
@@ -20,7 +23,9 @@ interface CatalogFiltersStore {
|
||||
updateFilter: () => void;
|
||||
initializeFilter: () => void;
|
||||
filteredCount: number;
|
||||
resetFilteredCount: () => void;
|
||||
filteredCids: Set<string>;
|
||||
incrementFilterCount: (filterIndex: number, cid: string) => void;
|
||||
recalcFilteredCount: () => void;
|
||||
}
|
||||
|
||||
const useCatalogFiltersStore = create(
|
||||
@@ -28,76 +33,109 @@ const useCatalogFiltersStore = create(
|
||||
(set, get) => ({
|
||||
showTextOnlyThreads: false,
|
||||
setShowTextOnlyThreads: (value: boolean) => {
|
||||
set({ showTextOnlyThreads: value, filteredCount: 0 });
|
||||
set({ showTextOnlyThreads: value });
|
||||
get().updateFilter();
|
||||
},
|
||||
filterText: '',
|
||||
setFilterText: (value: string) => set({ filterText: value }),
|
||||
filterItems: [],
|
||||
filteredCount: 0,
|
||||
filteredCids: new Set<string>(),
|
||||
setFilterItems: (items: FilterItem[]) => {
|
||||
const nonEmptyItems = items.filter((item) => item.text.trim() !== '');
|
||||
const nonEmptyItems = items
|
||||
.filter((item) => item.text.trim() !== '')
|
||||
.map((item) => ({
|
||||
...item,
|
||||
count: item.count || 0,
|
||||
filteredCids: item.filteredCids || new Set(),
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
}));
|
||||
set({ filterItems: nonEmptyItems });
|
||||
get().recalcFilteredCount();
|
||||
},
|
||||
saveAndApplyFilters: (items: FilterItem[]) => {
|
||||
const nonEmptyItems = items.filter((item) => item.text.trim() !== '');
|
||||
set({ filterItems: nonEmptyItems, filteredCount: 0 });
|
||||
const nonEmptyItems = items
|
||||
.filter((item) => item.text.trim() !== '')
|
||||
.map((item) => ({
|
||||
...item,
|
||||
count: item.count || 0,
|
||||
filteredCids: item.filteredCids || new Set(),
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
}));
|
||||
set({
|
||||
filterItems: nonEmptyItems,
|
||||
filteredCids: new Set<string>(),
|
||||
});
|
||||
get().recalcFilteredCount();
|
||||
get().updateFilter();
|
||||
},
|
||||
filter: undefined,
|
||||
updateFilter: () => {
|
||||
// const filteredCids = new Set<string>();
|
||||
set((state) => ({
|
||||
filter: (comment: Comment) => {
|
||||
const {
|
||||
showTextOnlyThreads,
|
||||
// filterItems
|
||||
} = state;
|
||||
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
|
||||
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link);
|
||||
// const title = comment?.title?.toLowerCase() || '';
|
||||
// const content = comment?.content?.toLowerCase() || '';
|
||||
|
||||
// const matchesFilterItems = filterItems
|
||||
// .filter((item) => item.enabled)
|
||||
// .some((item) => {
|
||||
// const text = item.text.toLowerCase();
|
||||
// return title.includes(text) || content.includes(text);
|
||||
// });
|
||||
|
||||
const shouldShow = showTextOnlyThreads || hasThumbnail;
|
||||
|
||||
if (
|
||||
!shouldShow
|
||||
// || matchesFilterItems
|
||||
) {
|
||||
// if (
|
||||
// matchesFilterItems &&
|
||||
// !filteredCids.has(comment.cid)) {
|
||||
// filteredCids.add(comment.cid);
|
||||
// set((state) => ({ filteredCount: state.filteredCount + 1 }));
|
||||
// }
|
||||
return false;
|
||||
if (!comment?.cid) return true;
|
||||
const { filterItems } = state;
|
||||
let shouldHide = false;
|
||||
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 (item.hide) {
|
||||
shouldHide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return !shouldHide;
|
||||
},
|
||||
}));
|
||||
},
|
||||
initializeFilter: () => {
|
||||
const { updateFilter } = get();
|
||||
updateFilter();
|
||||
get().updateFilter();
|
||||
},
|
||||
incrementFilterCount: (filterIndex: number, cid: string) => {
|
||||
set((state) => {
|
||||
const newFilterItems = [...state.filterItems];
|
||||
if (newFilterItems[filterIndex]) {
|
||||
const item = newFilterItems[filterIndex];
|
||||
if (!item.filteredCids.has(cid)) {
|
||||
const newItemFilteredCids = new Set(item.filteredCids);
|
||||
newItemFilteredCids.add(cid);
|
||||
newFilterItems[filterIndex] = {
|
||||
...item,
|
||||
count: item.count + 1,
|
||||
filteredCids: newItemFilteredCids,
|
||||
};
|
||||
const newFilteredCount = newFilterItems.reduce((sum, filt) => (filt.hide ? sum + filt.count : sum), 0);
|
||||
return { filterItems: newFilterItems, filteredCount: newFilteredCount };
|
||||
}
|
||||
}
|
||||
return state;
|
||||
});
|
||||
},
|
||||
recalcFilteredCount: () => {
|
||||
set((state) => ({
|
||||
filteredCount: state.filterItems.reduce((sum, item) => (item.hide ? sum + item.count : sum), 0),
|
||||
}));
|
||||
},
|
||||
filteredCount: 0,
|
||||
resetFilteredCount: () => set({ filteredCount: 0 }),
|
||||
}),
|
||||
{
|
||||
name: 'catalog-filters-storage',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state) {
|
||||
state.updateFilter();
|
||||
}
|
||||
partialize: (state) => {
|
||||
return {
|
||||
showTextOnlyThreads: state.showTextOnlyThreads,
|
||||
filterItems: state.filterItems.map((item) => ({
|
||||
text: item.text,
|
||||
enabled: item.enabled,
|
||||
hide: item.hide,
|
||||
top: item.top,
|
||||
})),
|
||||
} as any;
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 340px;
|
||||
width: 380px;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
@@ -51,6 +51,12 @@
|
||||
border-left: var(--settings-modal-border-left);
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.modal {
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
@@ -142,7 +148,7 @@
|
||||
outline: none;
|
||||
border: var(--reply-modal-field-input-border, revert);
|
||||
font-size: 11px;
|
||||
width: 200px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.filtersTable input[type='text']:focus {
|
||||
@@ -164,3 +170,22 @@
|
||||
.saveButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.filterHits {
|
||||
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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||
import styles from './catalog-filters.module.css';
|
||||
@@ -7,16 +7,44 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { filterItems, saveAndApplyFilters } = useCatalogFiltersStore();
|
||||
|
||||
const [localFilterItems, setLocalFilterItems] = useState(filterItems);
|
||||
const [localFilterItems, setLocalFilterItems] = useState(
|
||||
filterItems.map((item) => ({
|
||||
...item,
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
})),
|
||||
);
|
||||
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
// Update local items when store changes
|
||||
useEffect(() => {
|
||||
setLocalFilterItems(
|
||||
filterItems.map((item) => ({
|
||||
...item,
|
||||
hide: item.hide ?? true,
|
||||
top: item.top ?? false,
|
||||
})),
|
||||
);
|
||||
}, [filterItems]);
|
||||
|
||||
const handleAddFilter = useCallback(() => {
|
||||
setLocalFilterItems((prev) => {
|
||||
const newIndex = prev.length;
|
||||
setTimeout(() => {
|
||||
inputRefs.current[newIndex]?.focus();
|
||||
}, 0);
|
||||
return [...prev, { text: '', enabled: true }];
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
text: '',
|
||||
enabled: true,
|
||||
count: 0,
|
||||
filteredCids: new Set<string>(),
|
||||
hide: true,
|
||||
top: false,
|
||||
},
|
||||
];
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -47,10 +75,13 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
<table className={styles.filtersTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('order')}</th>
|
||||
<th>{t('enable')}</th>
|
||||
<th>{t('text')}</th>
|
||||
<th>{t('delete')}</th>
|
||||
<th>order</th>
|
||||
<th>on</th>
|
||||
<th>pattern</th>
|
||||
<th>color</th>
|
||||
<th>hide</th>
|
||||
<th>top</th>
|
||||
<th>del</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -80,17 +111,27 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
|
||||
ref={(el) => (inputRefs.current[index] = el)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span className={styles.clickbox} />
|
||||
</td>
|
||||
<td>
|
||||
<input type='checkbox' checked={item.hide} onChange={(e) => updateLocalFilterItem(index, { ...item, hide: e.target.checked })} />
|
||||
</td>
|
||||
<td>
|
||||
<input type='checkbox' checked={item.top} onChange={(e) => updateLocalFilterItem(index, { ...item, top: e.target.checked })} />
|
||||
</td>
|
||||
<td>
|
||||
<span className={styles.deleteButton} onClick={() => removeLocalFilterItem(index)}>
|
||||
×
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.filterHits}>{item.count > 0 && `x${item.count}`}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={4}>
|
||||
<td colSpan={7}>
|
||||
<button className={styles.addButton} onClick={handleAddFilter}>
|
||||
{t('add')}
|
||||
</button>
|
||||
@@ -112,7 +153,7 @@ const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||
<div className={styles.overlay} onClick={closeModal} />
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>{t('filters')}</span>
|
||||
<span className={styles.title}>{t('filters_and_highlights')}</span>
|
||||
<span className={styles.closeButton} title='close' onClick={closeModal} />
|
||||
</div>
|
||||
<FiltersTable onSave={closeModal} />
|
||||
@@ -125,13 +166,23 @@ const CatalogFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const closeModal = () => {
|
||||
const closeModal = useCallback(() => {
|
||||
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(false)} style={{ cursor: 'not-allowed' }}>
|
||||
<span className={`${styles.filtersButton} button`} onClick={() => setShowModal(true)}>
|
||||
{t('filters')}
|
||||
</span>
|
||||
{showModal && <FiltersModal closeModal={closeModal} />}
|
||||
|
||||
+138
-20
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks';
|
||||
@@ -14,22 +14,97 @@ import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import useInterfaceSettingsStore from '../../stores/use-interface-settings-store';
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
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';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
|
||||
const createThreadsWithoutImagesFilter = () => ({
|
||||
filter: (comment: Comment) => {
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
key: 'threads-with-images-only',
|
||||
});
|
||||
const createContentFilter = (
|
||||
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
|
||||
onFilterMatch?: (filterIndex: number, cid: string) => void,
|
||||
) => {
|
||||
// Create a unique key based on the enabled filter items
|
||||
const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '');
|
||||
const filterKey =
|
||||
enabledFilters.length > 0
|
||||
? `content-filter-${enabledFilters.map((item) => `${item.text}-${item.hide ? 'hide' : ''}-${item.top ? 'top' : ''}`).join('-')}`
|
||||
: 'no-content-filter';
|
||||
|
||||
return {
|
||||
filter: (comment: Comment) => {
|
||||
if (!comment?.cid) return true;
|
||||
|
||||
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();
|
||||
|
||||
if (titleLower.includes(pattern) || contentLower.includes(pattern)) {
|
||||
// Find the original filter index to increment count
|
||||
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
|
||||
if (filterIndex !== -1) {
|
||||
if (onFilterMatch) {
|
||||
onFilterMatch(filterIndex, comment.cid);
|
||||
} else {
|
||||
// Fallback to the store method if no callback provided
|
||||
useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid);
|
||||
}
|
||||
}
|
||||
|
||||
// If this filter is set to hide, filter out the comment
|
||||
if (item.hide) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this filter is set to top, we'll handle it separately in the component
|
||||
// (we don't filter it out here)
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
key: filterKey,
|
||||
};
|
||||
};
|
||||
|
||||
const createImageFilter = (showTextOnlyThreads: boolean) => {
|
||||
return {
|
||||
filter: (comment: Comment) => {
|
||||
if (showTextOnlyThreads) return true;
|
||||
|
||||
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
|
||||
const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link);
|
||||
|
||||
return hasThumbnail;
|
||||
},
|
||||
key: showTextOnlyThreads ? 'no-image-filter' : 'threads-with-images-only',
|
||||
};
|
||||
};
|
||||
|
||||
const createCombinedFilter = (
|
||||
showTextOnlyThreads: boolean,
|
||||
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean }[],
|
||||
onFilterMatch?: (filterIndex: number, cid: string) => void,
|
||||
) => {
|
||||
const imageFilter = createImageFilter(showTextOnlyThreads);
|
||||
const contentFilter = createContentFilter(filterItems, onFilterMatch);
|
||||
|
||||
return {
|
||||
filter: (comment: Comment) => {
|
||||
if (!imageFilter.filter(comment)) return false;
|
||||
|
||||
return contentFilter.filter(comment);
|
||||
},
|
||||
key: `${imageFilter.key}-${contentFilter.key}`,
|
||||
};
|
||||
};
|
||||
|
||||
const Catalog = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,7 +114,7 @@ const Catalog = () => {
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const defaultSubplebbits = useDefaultSubplebbits();
|
||||
const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore();
|
||||
const { hideThreadsWithoutImages } = useInterfaceSettingsStore();
|
||||
const { showTextOnlyThreads, filterItems } = useCatalogFiltersStore();
|
||||
|
||||
const account = useAccount();
|
||||
const subscriptions = account?.subscriptions;
|
||||
@@ -77,12 +152,17 @@ 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);
|
||||
}, []);
|
||||
|
||||
const feedOptions = useMemo(() => {
|
||||
const options: any = {
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
|
||||
filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined,
|
||||
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
|
||||
};
|
||||
|
||||
if (isInAllView || isInSubscriptionsView) {
|
||||
@@ -90,7 +170,7 @@ const Catalog = () => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, hideThreadsWithoutImages]);
|
||||
}, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, showTextOnlyThreads, filterItems, handleFilterMatch]);
|
||||
|
||||
const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions);
|
||||
const { accountComments } = useAccountComments();
|
||||
@@ -108,13 +188,13 @@ const Catalog = () => {
|
||||
timestamp > Date.now() / 1000 - 60 * 60 &&
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
(hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
|
||||
(showTextOnlyThreads ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) &&
|
||||
cid === postCid &&
|
||||
comment?.subplebbitAddress === subplebbitAddress &&
|
||||
!feed.some((post) => post.cid === cid)
|
||||
);
|
||||
}),
|
||||
[accountComments, subplebbitAddress, feed, hideThreadsWithoutImages],
|
||||
[accountComments, subplebbitAddress, feed, showTextOnlyThreads],
|
||||
);
|
||||
|
||||
// show newest account comment at the top of the feed but after pinned posts
|
||||
@@ -146,13 +226,14 @@ const Catalog = () => {
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 7,
|
||||
filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined,
|
||||
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
|
||||
});
|
||||
|
||||
const { feed: monthlyFeed } = useFeed({
|
||||
subplebbitAddresses,
|
||||
sortType,
|
||||
newerThan: 60 * 60 * 24 * 30,
|
||||
filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined,
|
||||
filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
|
||||
});
|
||||
|
||||
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
|
||||
@@ -265,7 +346,44 @@ const Catalog = () => {
|
||||
|
||||
const isFeedLoaded = feed.length > 0 || state === 'failed';
|
||||
|
||||
const rows = useCatalogFeedRows(columnCount, combinedFeed, isFeedLoaded, subplebbit);
|
||||
// Process the feed to move "top" posts to the top
|
||||
const processedFeed = useMemo(() => {
|
||||
if (!combinedFeed || combinedFeed.length === 0) return combinedFeed;
|
||||
|
||||
const enabledTopFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '' && item.top);
|
||||
if (enabledTopFilters.length === 0) return combinedFeed;
|
||||
|
||||
// Separate posts that match "top" filters
|
||||
const topPosts: Comment[] = [];
|
||||
const regularPosts: Comment[] = [];
|
||||
|
||||
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)) {
|
||||
isTop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isTop) {
|
||||
topPosts.push(comment);
|
||||
} else {
|
||||
regularPosts.push(comment);
|
||||
}
|
||||
});
|
||||
|
||||
// Return top posts followed by regular posts
|
||||
return [...topPosts, ...regularPosts];
|
||||
}, [combinedFeed, filterItems]);
|
||||
|
||||
const rows = useCatalogFeedRows(columnCount, processedFeed, isFeedLoaded, subplebbit);
|
||||
|
||||
// save the last Virtuoso state to restore it when navigating back
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
@@ -293,7 +411,7 @@ const Catalog = () => {
|
||||
<div className={styles.content}>
|
||||
<hr />
|
||||
<div className={styles.catalog}>
|
||||
{combinedFeed.length !== 0 ? (
|
||||
{processedFeed.length !== 0 ? (
|
||||
<>
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
|
||||
Reference in New Issue
Block a user