diff --git a/src/app.tsx b/src/app.tsx
index 3843ee3b..b3810f16 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -55,8 +55,9 @@ const BoardLayout = () => {
{isMobile
? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<>
-
+
+
>
)
: (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
diff --git a/src/components/board-buttons/board-buttons.module.css b/src/components/board-buttons/board-buttons.module.css
index 43068ba4..59d754e4 100644
--- a/src/components/board-buttons/board-buttons.module.css
+++ b/src/components/board-buttons/board-buttons.module.css
@@ -51,7 +51,6 @@
}
.desktopBoardButtons {
- padding-right: 5px;
text-transform: capitalize;
}
diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx
index 073a504d..320a414c 100644
--- a/src/components/board-buttons/board-buttons.tsx
+++ b/src/components/board-buttons/board-buttons.tsx
@@ -4,17 +4,18 @@ import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
+import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
import useSortingStore from '../../stores/use-sorting-store';
-import useTimeFilter from '../../hooks/use-time-filter';
-import CatalogFilters from '../../views/catalog/catalog-filters/';
-import styles from './board-buttons.module.css';
-import Tooltip from '../tooltip';
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 useTimeFilter from '../../hooks/use-time-filter';
+import CatalogFilters from '../catalog-filters';
+import CatalogSearch from '../catalog-search';
+import Tooltip from '../tooltip';
+import styles from './board-buttons.module.css';
+import _ from 'lodash';
interface BoardButtonsProps {
address?: string | undefined;
@@ -282,7 +283,7 @@ export const MobileBoardButtons = () => {
-
+
>
@@ -380,17 +381,16 @@ export const DesktopBoardButtons = () => {
{(isInAllView || isInSubscriptionsView) && (
)}
- {isInCatalogView && (
- <>
- [
- ]
- >
- )}{' '}
{!(isInAllView || isInSubscriptionsView) && (
<>
[
]
>
+ )}{' '}
+ {isInCatalogView && (
+ <>
+ []
+ >
)}
>
diff --git a/src/views/catalog/catalog-filters/catalog-filters.module.css b/src/components/catalog-filters/catalog-filters.module.css
similarity index 100%
rename from src/views/catalog/catalog-filters/catalog-filters.module.css
rename to src/components/catalog-filters/catalog-filters.module.css
diff --git a/src/views/catalog/catalog-filters/catalog-filters.tsx b/src/components/catalog-filters/catalog-filters.tsx
similarity index 96%
rename from src/views/catalog/catalog-filters/catalog-filters.tsx
rename to src/components/catalog-filters/catalog-filters.tsx
index fc403549..188ec343 100644
--- a/src/views/catalog/catalog-filters/catalog-filters.tsx
+++ b/src/components/catalog-filters/catalog-filters.tsx
@@ -1,6 +1,6 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store';
+import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import styles from './catalog-filters.module.css';
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
@@ -41,6 +41,8 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
enabled: true,
count: 0,
filteredCids: new Set(),
+ subplebbitCounts: new Map(),
+ subplebbitFilteredCids: new Map>(),
hide: true,
top: false,
},
diff --git a/src/views/catalog/catalog-filters/index.ts b/src/components/catalog-filters/index.ts
similarity index 100%
rename from src/views/catalog/catalog-filters/index.ts
rename to src/components/catalog-filters/index.ts
diff --git a/src/components/catalog-search/catalog-search.module.css b/src/components/catalog-search/catalog-search.module.css
new file mode 100644
index 00000000..6c492561
--- /dev/null
+++ b/src/components/catalog-search/catalog-search.module.css
@@ -0,0 +1,49 @@
+.button {
+ text-transform: capitalize;
+ color: var(--button-desktop-text-color);
+ text-decoration: var(--button-text-decoration);
+}
+
+.button:hover {
+ color: var(--button-desktop-text-color-hover);
+ cursor: pointer;
+}
+
+.filtersButton {
+ text-transform: capitalize;
+}
+
+.searchContainer {
+ display: inline;
+}
+
+.searchContainer input {
+ width: 75px;
+ margin: 0px 5px;
+ padding: 1px;
+ outline: none;
+ border: 1px solid #AAA;
+ font-size: 11px;
+}
+
+.searchContainer input:focus {
+ border: var(--post-form-field-input-focus-border);
+}
+
+.closeSearch {
+ cursor: pointer;
+ border: none;
+ white-space: nowrap;
+ color: var(--button-desktop-text-color);
+}
+
+.closeSearch:hover {
+ color: var(--button-desktop-text-color-hover);
+}
+
+@media (max-width: 640px) {
+ .searchContainer input {
+ width: 75px;
+ font-size: 16px;
+ }
+}
diff --git a/src/components/catalog-search/catalog-search.tsx b/src/components/catalog-search/catalog-search.tsx
new file mode 100644
index 00000000..7c723dfe
--- /dev/null
+++ b/src/components/catalog-search/catalog-search.tsx
@@ -0,0 +1,45 @@
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import styles from './catalog-search.module.css';
+import useIsMobile from '../../hooks/use-is-mobile';
+
+const CatalogSearch = () => {
+ const { t } = useTranslation();
+ const [openSearch, setOpenSearch] = useState(false);
+
+ useEffect(() => {
+ if (openSearch) {
+ const input = document.querySelector('input');
+ input?.focus();
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ setOpenSearch(false);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ }
+ }, [openSearch]);
+
+ const isMobile = useIsMobile();
+
+ return (
+ <>
+ {!isMobile && '['}
+ setOpenSearch(!openSearch)}>
+ {t('search')}
+
+ {!isMobile && ']'}
+ {openSearch && (
+
+
+ setOpenSearch(false)}>
+ ✖
+
+
+ )}
+ >
+ );
+};
+
+export default CatalogSearch;
diff --git a/src/components/catalog-search/index.ts b/src/components/catalog-search/index.ts
new file mode 100644
index 00000000..aa2ace2c
--- /dev/null
+++ b/src/components/catalog-search/index.ts
@@ -0,0 +1 @@
+export { default } from './catalog-search';
diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css
index 7dca9193..649c43c1 100644
--- a/src/components/post-form/post-form.module.css
+++ b/src/components/post-form/post-form.module.css
@@ -15,8 +15,7 @@
.postFormMobile {
text-align: center;
- margin-top: -2px;
- padding-bottom: 7px;
+ margin-top: -3px;
}
.closed {
diff --git a/src/stores/use-catalog-filters-store.ts b/src/stores/use-catalog-filters-store.ts
index 0d05f31b..1b7d9728 100644
--- a/src/stores/use-catalog-filters-store.ts
+++ b/src/stores/use-catalog-filters-store.ts
@@ -7,6 +7,8 @@ interface FilterItem {
enabled: boolean;
count: number;
filteredCids: Set;
+ subplebbitCounts: Map;
+ subplebbitFilteredCids: Map>;
hide: boolean;
top: boolean;
}
@@ -24,8 +26,11 @@ interface CatalogFiltersStore {
initializeFilter: () => void;
filteredCount: number;
filteredCids: Set;
- incrementFilterCount: (filterIndex: number, cid: string) => void;
+ incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => void;
recalcFilteredCount: () => void;
+ currentSubplebbitAddress: string | null;
+ setCurrentSubplebbitAddress: (address: string | null) => void;
+ getFilteredCountForCurrentSubplebbit: () => number;
}
const useCatalogFiltersStore = create(
@@ -41,6 +46,8 @@ const useCatalogFiltersStore = create(
filterItems: [],
filteredCount: 0,
filteredCids: new Set(),
+ currentSubplebbitAddress: null,
+ setCurrentSubplebbitAddress: (address: string | null) => set({ currentSubplebbitAddress: address }),
setFilterItems: (items: FilterItem[]) => {
const nonEmptyItems = items
.filter((item) => item.text.trim() !== '')
@@ -48,6 +55,8 @@ const useCatalogFiltersStore = create(
...item,
count: item.count || 0,
filteredCids: item.filteredCids || new Set(),
+ subplebbitCounts: item.subplebbitCounts || new Map(),
+ subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true,
top: item.top ?? false,
}));
@@ -61,6 +70,8 @@ const useCatalogFiltersStore = create(
...item,
count: item.count || 0,
filteredCids: item.filteredCids || new Set(),
+ subplebbitCounts: item.subplebbitCounts || new Map(),
+ subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(),
hide: item.hide ?? true,
top: item.top ?? false,
}));
@@ -98,30 +109,75 @@ const useCatalogFiltersStore = create(
initializeFilter: () => {
get().updateFilter();
},
- incrementFilterCount: (filterIndex: number, cid: string) => {
+ incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => {
set((state) => {
const newFilterItems = [...state.filterItems];
if (newFilterItems[filterIndex]) {
const item = newFilterItems[filterIndex];
- if (!item.filteredCids.has(cid)) {
+
+ const subplebbitFilteredCids = new Map(item.subplebbitFilteredCids);
+ if (!subplebbitFilteredCids.has(subplebbitAddress)) {
+ subplebbitFilteredCids.set(subplebbitAddress, new Set());
+ }
+ const cidSet = subplebbitFilteredCids.get(subplebbitAddress)!;
+
+ if (!cidSet.has(cid)) {
const newItemFilteredCids = new Set(item.filteredCids);
newItemFilteredCids.add(cid);
+
+ cidSet.add(cid);
+ subplebbitFilteredCids.set(subplebbitAddress, cidSet);
+
+ const subplebbitCounts = new Map(item.subplebbitCounts);
+ const currentCount = subplebbitCounts.get(subplebbitAddress) || 0;
+ subplebbitCounts.set(subplebbitAddress, currentCount + 1);
+
newFilterItems[filterIndex] = {
...item,
count: item.count + 1,
filteredCids: newItemFilteredCids,
+ subplebbitCounts,
+ subplebbitFilteredCids,
};
- const newFilteredCount = newFilterItems.reduce((sum, filt) => (filt.hide ? sum + filt.count : sum), 0);
- return { filterItems: newFilterItems, filteredCount: newFilteredCount };
+
+ return { filterItems: newFilterItems };
}
}
return state;
});
+
+ get().recalcFilteredCount();
},
recalcFilteredCount: () => {
- set((state) => ({
- filteredCount: state.filterItems.reduce((sum, item) => (item.hide ? sum + item.count : sum), 0),
- }));
+ set((state) => {
+ const currentSubplebbit = state.currentSubplebbitAddress;
+ if (!currentSubplebbit) return { filteredCount: 0 };
+
+ let filteredCount = 0;
+ for (const item of state.filterItems) {
+ if (item.enabled && item.hide) {
+ const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0;
+ filteredCount += subCount;
+ }
+ }
+
+ return { filteredCount };
+ });
+ },
+ getFilteredCountForCurrentSubplebbit: () => {
+ const state = get();
+ const currentSubplebbit = state.currentSubplebbitAddress;
+ if (!currentSubplebbit) return 0;
+
+ let filteredCount = 0;
+ for (const item of state.filterItems) {
+ if (item.enabled && item.hide) {
+ const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0;
+ filteredCount += subCount;
+ }
+ }
+
+ return filteredCount;
},
}),
{
@@ -134,6 +190,8 @@ const useCatalogFiltersStore = create(
enabled: item.enabled,
hide: item.hide,
top: item.top,
+ subplebbitCounts: Array.from(item.subplebbitCounts || new Map()),
+ subplebbitFilteredCids: Array.from((item.subplebbitFilteredCids || new Map()).entries()).map(([key, value]) => [key, Array.from(value)]),
})),
} as any;
},
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx
index 16f8da82..64b60d83 100644
--- a/src/views/catalog/catalog.tsx
+++ b/src/views/catalog/catalog.tsx
@@ -23,7 +23,8 @@ const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
const createContentFilter = (
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean }[],
- onFilterMatch?: (filterIndex: number, cid: string) => void,
+ subplebbitAddress: string,
+ onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => {
// Create a unique key based on the enabled filter items
const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '');
@@ -51,10 +52,10 @@ const createContentFilter = (
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
if (filterIndex !== -1) {
if (onFilterMatch) {
- onFilterMatch(filterIndex, comment.cid);
+ onFilterMatch(filterIndex, comment.cid, subplebbitAddress);
} else {
// Fallback to the store method if no callback provided
- useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid);
+ useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, subplebbitAddress);
}
}
@@ -91,10 +92,11 @@ const createImageFilter = (showTextOnlyThreads: boolean) => {
const createCombinedFilter = (
showTextOnlyThreads: boolean,
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean }[],
- onFilterMatch?: (filterIndex: number, cid: string) => void,
+ subplebbitAddress: string,
+ onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void,
) => {
const imageFilter = createImageFilter(showTextOnlyThreads);
- const contentFilter = createContentFilter(filterItems, onFilterMatch);
+ const contentFilter = createContentFilter(filterItems, subplebbitAddress, onFilterMatch);
return {
filter: (comment: Comment) => {
@@ -152,17 +154,25 @@ 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);
+ // Create a stable callback for filter matching
+ const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => {
+ useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid, subplebbitAddress);
}, []);
+ // Set the current subplebbit address
+ useEffect(() => {
+ useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(subplebbitAddress || null);
+ return () => {
+ useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(null);
+ };
+ }, [subplebbitAddress]);
+
const feedOptions = useMemo(() => {
const options: any = {
subplebbitAddresses,
sortType,
postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage,
- filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
+ filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
};
if (isInAllView || isInSubscriptionsView) {
@@ -170,7 +180,18 @@ const Catalog = () => {
}
return options;
- }, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, showTextOnlyThreads, filterItems, handleFilterMatch]);
+ }, [
+ subplebbitAddresses,
+ sortType,
+ isInAllView,
+ isInSubscriptionsView,
+ postsPerPage,
+ timeFilterSeconds,
+ showTextOnlyThreads,
+ filterItems,
+ subplebbitAddress,
+ handleFilterMatch,
+ ]);
const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions);
const { accountComments } = useAccountComments();
@@ -226,14 +247,14 @@ const Catalog = () => {
subplebbitAddresses,
sortType,
newerThan: 60 * 60 * 24 * 7,
- filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
+ filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
});
const { feed: monthlyFeed } = useFeed({
subplebbitAddresses,
sortType,
newerThan: 60 * 60 * 24 * 30,
- filter: createCombinedFilter(showTextOnlyThreads, filterItems, handleFilterMatch),
+ filter: createCombinedFilter(showTextOnlyThreads, filterItems, subplebbitAddress || 'all', handleFilterMatch),
});
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);