mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(catalog): add text pattern filters
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
interface CatalogFiltersStore {
|
interface CatalogFiltersStore {
|
||||||
showAdultBoards: boolean;
|
showAdultBoards: boolean;
|
||||||
@@ -7,24 +8,26 @@ interface CatalogFiltersStore {
|
|||||||
setShowGoreBoards: (value: boolean) => void;
|
setShowGoreBoards: (value: boolean) => void;
|
||||||
showTextOnlyThreads: boolean;
|
showTextOnlyThreads: boolean;
|
||||||
setShowTextOnlyThreads: (value: boolean) => void;
|
setShowTextOnlyThreads: (value: boolean) => void;
|
||||||
|
filterText: string;
|
||||||
|
setFilterText: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useCatalogFiltersStore = create<CatalogFiltersStore>((set) => ({
|
const useCatalogFiltersStore = create(
|
||||||
showTextOnlyThreads: localStorage.getItem('showTextOnlyThreads') === 'true' ? true : false,
|
persist<CatalogFiltersStore>(
|
||||||
setShowTextOnlyThreads: (value: boolean) => {
|
(set) => ({
|
||||||
set({ showTextOnlyThreads: value });
|
showTextOnlyThreads: false,
|
||||||
localStorage.setItem('showTextOnlyThreads', value.toString());
|
setShowTextOnlyThreads: (value: boolean) => set({ showTextOnlyThreads: value }),
|
||||||
|
showAdultBoards: false,
|
||||||
|
setShowAdultBoards: (value: boolean) => set({ showAdultBoards: value }),
|
||||||
|
showGoreBoards: false,
|
||||||
|
setShowGoreBoards: (value: boolean) => set({ showGoreBoards: value }),
|
||||||
|
filterText: '',
|
||||||
|
setFilterText: (value: string) => set({ filterText: value }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'catalog-filters-storage',
|
||||||
},
|
},
|
||||||
showAdultBoards: localStorage.getItem('showAdultBoards') === 'true' ? true : false,
|
),
|
||||||
setShowAdultBoards: (value: boolean) => {
|
);
|
||||||
set({ showAdultBoards: value });
|
|
||||||
localStorage.setItem('showAdultBoards', value.toString());
|
|
||||||
},
|
|
||||||
showGoreBoards: localStorage.getItem('showGoreBoards') === 'true' ? true : false,
|
|
||||||
setShowGoreBoards: (value: boolean) => {
|
|
||||||
set({ showGoreBoards: value });
|
|
||||||
localStorage.setItem('showGoreBoards', value.toString());
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default useCatalogFiltersStore;
|
export default useCatalogFiltersStore;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filters label {
|
.filters label {
|
||||||
display: block;
|
display: inline-block;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,3 +101,62 @@
|
|||||||
.paddingBottom {
|
.paddingBottom {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable {
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 1px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable th {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable tbody td {
|
||||||
|
padding: 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable td {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable input[type='checkbox'] {
|
||||||
|
display: inline-block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable input[type='text'] {
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 1px;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
font-size: 11px;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersTable tfoot td {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,49 @@ import { isAllView, isCatalogView } from '../../../lib/utils/view-utils';
|
|||||||
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
|
||||||
import styles from './catalog-filters.module.css';
|
import styles from './catalog-filters.module.css';
|
||||||
|
|
||||||
|
const FiltersTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { filterText, setFilterText } = useCatalogFiltersStore();
|
||||||
|
const [localFilterText, setLocalFilterText] = useState(filterText);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={styles.filtersTable}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order</th>
|
||||||
|
<th>On</th>
|
||||||
|
<th>Pattern</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span className={styles.orderButton}>↑</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type='checkbox' className={styles.onCheckbox} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type='text' onChange={(e) => setLocalFilterText(e.target.value)} value={localFilterText} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className={styles.deleteButton}>×</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={9}>
|
||||||
|
<button className={styles.addButton}>Add</button>
|
||||||
|
<button className={styles.saveButton}>Save</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showAdultBoards, setShowAdultBoards, showGoreBoards, setShowGoreBoards, showTextOnlyThreads, setShowTextOnlyThreads } = useCatalogFiltersStore();
|
const { showAdultBoards, setShowAdultBoards, showGoreBoards, setShowGoreBoards, showTextOnlyThreads, setShowTextOnlyThreads } = useCatalogFiltersStore();
|
||||||
@@ -23,24 +66,30 @@ const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
{isInCatalogView && (
|
{isInCatalogView && (
|
||||||
<label className={`${styles.paddingBottom} capitalize`}>
|
<div>
|
||||||
|
<label className='capitalize'>
|
||||||
<input type='checkbox' checked={!showTextOnlyThreads} onChange={(e) => setShowTextOnlyThreads(!e.target.checked)} />
|
<input type='checkbox' checked={!showTextOnlyThreads} onChange={(e) => setShowTextOnlyThreads(!e.target.checked)} />
|
||||||
{t('hide_threads_without_images')}
|
{t('hide_threads_without_images')}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isInAllView && (
|
{isInAllView && (
|
||||||
<div className={styles.nsfwLabels}>
|
<div className={styles.nsfwLabels}>
|
||||||
<div className={styles.categoryTitle}>{t('nsfw_boards')}</div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
<input type='checkbox' checked={!showGoreBoards} onChange={(e) => setShowGoreBoards(!e.target.checked)} />
|
<input type='checkbox' checked={!showGoreBoards} onChange={(e) => setShowGoreBoards(!e.target.checked)} />
|
||||||
{t('hide_gore_boards')}
|
hide gore content
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type='checkbox' checked={!showAdultBoards} onChange={(e) => setShowAdultBoards(!e.target.checked)} />
|
|
||||||
{t('hide_adult_boards')}
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type='checkbox' checked={!showAdultBoards} onChange={(e) => setShowAdultBoards(!e.target.checked)} />
|
||||||
|
hide adult content
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<FiltersTable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -21,12 +21,21 @@ import styles from './catalog.module.css';
|
|||||||
|
|
||||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||||
|
|
||||||
const catalogFilter = (comment: Comment) => {
|
const threadsWithImagesFilter = (comment: Comment) => {
|
||||||
const commentMediaInfo = getCommentMediaInfo(comment);
|
const commentMediaInfo = getCommentMediaInfo(comment);
|
||||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, comment?.link);
|
const hasThumbnail = getHasThumbnail(commentMediaInfo, comment?.link);
|
||||||
return hasThumbnail;
|
return hasThumbnail;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const textFilter = (comment: Comment, filterText: string) => {
|
||||||
|
if (!filterText) return true;
|
||||||
|
const filterWords = filterText.toLowerCase().split(/\s+/);
|
||||||
|
const titleWords = comment?.title?.toLowerCase().split(/\s+/) || [];
|
||||||
|
const contentWords = comment?.content?.toLowerCase().split(/\s+/) || [];
|
||||||
|
|
||||||
|
return !filterWords.every((filterWord) => titleWords.includes(filterWord) || contentWords.includes(filterWord));
|
||||||
|
};
|
||||||
|
|
||||||
const Catalog = () => {
|
const Catalog = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -34,7 +43,7 @@ const Catalog = () => {
|
|||||||
|
|
||||||
const isInAllView = isAllView(location.pathname, useParams());
|
const isInAllView = isAllView(location.pathname, useParams());
|
||||||
const defaultSubplebbits = useDefaultSubplebbits();
|
const defaultSubplebbits = useDefaultSubplebbits();
|
||||||
const { showAdultBoards, showGoreBoards } = useCatalogFiltersStore();
|
const { showAdultBoards, showGoreBoards, showTextOnlyThreads, filterText } = useCatalogFiltersStore();
|
||||||
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const subscriptions = account?.subscriptions;
|
const subscriptions = account?.subscriptions;
|
||||||
@@ -72,15 +81,22 @@ const Catalog = () => {
|
|||||||
const postsPerPage = useMemo(() => (columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25), []);
|
const postsPerPage = useMemo(() => (columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25), []);
|
||||||
|
|
||||||
const { timeFilterSeconds } = useTimeFilter();
|
const { timeFilterSeconds } = useTimeFilter();
|
||||||
const { showTextOnlyThreads } = useCatalogFiltersStore();
|
|
||||||
const { sortType } = useSortingStore();
|
const { sortType } = useSortingStore();
|
||||||
|
|
||||||
const feedOptions: any = {
|
const feedOptions: any = useMemo(
|
||||||
|
() => ({
|
||||||
subplebbitAddresses,
|
subplebbitAddresses,
|
||||||
sortType,
|
sortType,
|
||||||
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
|
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
|
||||||
filter: !showTextOnlyThreads && catalogFilter,
|
filter: (comment: Comment) => {
|
||||||
};
|
if (!showTextOnlyThreads && !threadsWithImagesFilter(comment)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return textFilter(comment, filterText);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, showTextOnlyThreads, filterText],
|
||||||
|
);
|
||||||
|
|
||||||
if (isInAllView || isInSubscriptionsView) {
|
if (isInAllView || isInSubscriptionsView) {
|
||||||
feedOptions.newerThan = timeFilterSeconds;
|
feedOptions.newerThan = timeFilterSeconds;
|
||||||
|
|||||||
Reference in New Issue
Block a user