From 15bffe89bf7a82ef7d9d7b4c125136bd7bdeb87e Mon Sep 17 00:00:00 2001 From: "Tom (plebeius.eth)" Date: Sun, 9 Jun 2024 16:40:32 +0200 Subject: [PATCH] feat: add time filter to p/all and p/subscriptions --- src/app.tsx | 16 ++--- .../board-buttons/board-buttons.tsx | 62 +++++++++++++++++-- src/components/board-buttons/index.ts | 2 +- src/components/board-header/board-header.tsx | 2 +- .../post-menu-desktop/post-menu-desktop.tsx | 2 +- src/components/topbar/topbar.tsx | 18 ++++-- src/hooks/use-time-filter.js | 60 ++++++++++++++++++ src/lib/utils/view-utils.ts | 16 ++++- src/views/board/board.tsx | 10 +-- src/views/catalog/catalog.tsx | 10 +-- 10 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 src/hooks/use-time-filter.js diff --git a/src/app.tsx b/src/app.tsx index 4bc0d54e..52dc18f9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -92,16 +92,16 @@ const App = () => { } /> } /> - } /> + } /> + } /> } /> - } /> - } /> - } /> + } /> + } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 3bac6912..588e8383 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -1,9 +1,10 @@ import { useTranslation } from 'react-i18next'; -import { Link, useLocation, useParams } from 'react-router-dom'; +import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { isAllView, isCatalogView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; +import useTimeFilter from '../../hooks/use-time-filter'; import styles from './board-buttons.module.css'; interface BoardButtonsProps { @@ -55,6 +56,7 @@ const RefreshButton = () => { }; const SortOptions = () => { + const { t } = useTranslation(); const { sortType, setSortType } = useSortingStore(); const handleSortChange = (event: React.ChangeEvent) => { @@ -63,21 +65,54 @@ const SortOptions = () => { }; return ( <> - Sort by:  + {t('sort_by')}:    ); }; +export const TimeFilter = ({ isInCatalogView, isTopbar = false }: { isInCatalogView: boolean; isTopbar?: boolean }) => { + const { t } = useTranslation(); + const params = useParams(); + const navigate = useNavigate(); + const { timeFilterNames } = useTimeFilter(); + const selectedTimeFilterName = params.timeFilterName; + + const changeTimeFilter = (event: React.ChangeEvent) => { + const timeFilterName = event.target.value; + const link = isInCatalogView ? `/p/all/catalog/${timeFilterName}` : `/p/all/${timeFilterName}`; + navigate(link); + }; + + return ( + <> + {!isTopbar ? ( + <> + {t('time_filter')}:  + + ) : ( + <> + )} + + + ); +}; + export const MobileBoardButtons = () => { const params = useParams(); const location = useLocation(); const isInAllView = isAllView(location.pathname); - const isInCatalogView = isCatalogView(location.pathname); + const isInCatalogView = isCatalogView(location.pathname, params); const isInPendingPostPage = isPendingPostView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname); @@ -106,10 +141,20 @@ export const MobileBoardButtons = () => { <>
+ +   
)} + {(isInAllView || isInSubscriptionsView) && !isInCatalogView && ( + <> +
+
+ +
+ + )} )} @@ -121,7 +166,7 @@ export const DesktopBoardButtons = () => { const location = useLocation(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; - const isInCatalogView = isCatalogView(location.pathname); + const isInCatalogView = isCatalogView(location.pathname, params); const isInAllView = isAllView(location.pathname); const isInPendingPostPage = isPendingPostView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params); @@ -166,6 +211,11 @@ export const DesktopBoardButtons = () => { ] )} + {(isInAllView || isInSubscriptionsView) && ( + <> + + + )} )} diff --git a/src/components/board-buttons/index.ts b/src/components/board-buttons/index.ts index 067ee43b..ec349233 100644 --- a/src/components/board-buttons/index.ts +++ b/src/components/board-buttons/index.ts @@ -1 +1 @@ -export { MobileBoardButtons, DesktopBoardButtons } from './board-buttons'; +export { TimeFilter, MobileBoardButtons, DesktopBoardButtons } from './board-buttons'; diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index 242968b5..879a56ba 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -31,7 +31,7 @@ const BoardHeader = () => { const multisubMetadata = useMultisubMetadata(); - const title = isInAllView ? multisubMetadata?.title : isInSubscriptionsView ? 'Subscriptions' : subplebbit?.title; + const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : subplebbit?.title; const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : `p/${address}`; const isBoardOffline = subplebbit?.updatedAt && subplebbit.updatedAt < Date.now() / 1000 - 60 * 60; diff --git a/src/components/post/post-desktop/post-menu-desktop/post-menu-desktop.tsx b/src/components/post/post-desktop/post-menu-desktop/post-menu-desktop.tsx index aa6ae990..306e4435 100644 --- a/src/components/post/post-desktop/post-menu-desktop/post-menu-desktop.tsx +++ b/src/components/post/post-desktop/post-menu-desktop/post-menu-desktop.tsx @@ -114,7 +114,7 @@ const PostMenuDesktop = ({ post }: { post: Comment }) => { const location = useLocation(); const params = useParams(); const isInAllView = isAllView(location.pathname); - const isInCatalogView = isCatalogView(location.pathname); + const isInCatalogView = isCatalogView(location.pathname, params); const isInPostPageView = isPostPageView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname); diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index c5e5ba92..d6dd27c1 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -4,8 +4,10 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils'; import styles from './topbar.module.css'; +import useTimeFilter from '../../hooks/use-time-filter'; import useDefaultSubplebbits, { categorizeSubplebbits, useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import _, { debounce } from 'lodash'; +import { TimeFilter } from '../board-buttons'; const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => void }) => { const navigate = useNavigate(); @@ -55,7 +57,8 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => const TopBarDesktop = () => { const { t } = useTranslation(); const location = useLocation(); - const isInCatalogView = isCatalogView(location.pathname); + const params = useParams(); + const isInCatalogView = isCatalogView(location.pathname, params); const subplebbits = useDefaultSubplebbits(); const [showSearchBar, setShowSearchBar] = useState(false); @@ -74,8 +77,13 @@ const TopBarDesktop = () => { return (
- [all / subscriptions] [{renderSubplebbits(plebbitSubs)}] [{renderSubplebbits(projectsSubs)}] [ - {renderSubplebbits(interestsSubs)}] [{renderSubplebbits(randomSubs)}] [{renderSubplebbits(internationalSubs)}] + [all / subscriptions]{' '} + {subplebbits && ( + <> + [{renderSubplebbits(plebbitSubs)}] [{renderSubplebbits(projectsSubs)}] [{renderSubplebbits(interestsSubs)}] [{renderSubplebbits(randomSubs)}] [ + {renderSubplebbits(internationalSubs)}] + + )} [{t('settings')}] [ @@ -96,8 +104,9 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const currentSubplebbitIsInList = subplebbitAddresses.some((address: string) => address === subplebbitAddress); const location = useLocation(); + const params = useParams(); const isInAllView = isAllView(location.pathname); - const isInCatalogView = isCatalogView(location.pathname); + const isInCatalogView = isCatalogView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname); const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : subplebbitAddress; @@ -140,6 +149,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
{t('board')} {boardSelect} +
{t('settings')} diff --git a/src/hooks/use-time-filter.js b/src/hooks/use-time-filter.js new file mode 100644 index 00000000..7ff81fba --- /dev/null +++ b/src/hooks/use-time-filter.js @@ -0,0 +1,60 @@ +import assert from 'assert'; +import { useParams } from 'react-router-dom'; + +// the timestamp the last time the user visited +const lastVisitTimestamp = localStorage.getItem('plebchanLastVisitTimestamp'); + +// update the last visited timestamp every n seconds +setInterval(() => { + localStorage.setItem('plebchanLastVisitTimestamp', Date.now()); +}, 60 * 1000); + +const timeFilterNamesToSeconds = { + '1h': 60 * 60, + '12h': 60 * 60 * 12, + '24h': 60 * 60 * 24, + '48h': 60 * 60 * 24 * 2, + week: 60 * 60 * 24 * 7, + month: 60 * 60 * 24 * 30, + year: 60 * 60 * 24 * 365, + all: undefined, +}; + +// calculate the last visit timeFilterNamesToSeconds +const secondsSinceLastVisit = lastVisitTimestamp ? (Date.now() - lastVisitTimestamp) / 1000 : Infinity; +const day = 24 * 60 * 60; +let lastVisitTimeFilterName; +if (secondsSinceLastVisit > 30 * day) { + lastVisitTimeFilterName = 'month'; + timeFilterNamesToSeconds[lastVisitTimeFilterName] = timeFilterNamesToSeconds['month']; +} else if (secondsSinceLastVisit > 7 * day) { + const weeks = Math.ceil(secondsSinceLastVisit / day / 7); + lastVisitTimeFilterName = `${weeks}w`; + timeFilterNamesToSeconds[lastVisitTimeFilterName] = 60 * 60 * 24 * 7 * weeks; +} else if (secondsSinceLastVisit > day) { + const days = Math.ceil(secondsSinceLastVisit / day); + lastVisitTimeFilterName = `${days}d`; + timeFilterNamesToSeconds[lastVisitTimeFilterName] = 60 * 60 * 24 * days; +} else { + lastVisitTimeFilterName = '24h'; + timeFilterNamesToSeconds[lastVisitTimeFilterName] = timeFilterNamesToSeconds['24h']; +} + +const timeFilterNames = [lastVisitTimeFilterName, '1h', '12h', '24h', '48h', 'week', 'month', 'year', 'all']; + +const useTimeFilter = () => { + const params = useParams(); + let timeFilterName = params.timeFilterName; + + // the default time filter is the last visit time filter + if (!timeFilterName) { + timeFilterName = lastVisitTimeFilterName; + } + + assert(!timeFilterName || typeof timeFilterName === 'string', `useTimeFilter timeFilterName argument '${timeFilterName}' not a string`); + const timeFilterSeconds = timeFilterNamesToSeconds[timeFilterName]; + assert(!timeFilterName || timeFilterName === 'all' || timeFilterSeconds !== undefined, `useTimeFilter no filter for timeFilterName '${timeFilterName}'`); + return { timeFilterSeconds, timeFilterNames }; +}; + +export default useTimeFilter; diff --git a/src/lib/utils/view-utils.ts b/src/lib/utils/view-utils.ts index a990c9c5..7d8429b2 100644 --- a/src/lib/utils/view-utils.ts +++ b/src/lib/utils/view-utils.ts @@ -2,6 +2,7 @@ export type ParamsType = { accountCommentIndex?: string; commentCid?: string; subplebbitAddress?: string; + timeFilterName?: string; }; export const isAllView = (pathname: string): boolean => { @@ -14,8 +15,17 @@ export const isBoardView = (pathname: string, params: ParamsType): boolean => { return params.subplebbitAddress ? decodedPathname.startsWith(`/p/${params.subplebbitAddress}`) : false; }; -export const isCatalogView = (pathname: string): boolean => { - return pathname.endsWith('/catalog') || pathname.endsWith('/catalog/settings'); +export const isCatalogView = (pathname: string, params: ParamsType): boolean => { + const { subplebbitAddress, timeFilterName } = params; + const decodedPathname = decodeURIComponent(pathname); + + return ( + decodedPathname === `/p/${subplebbitAddress}/catalog` || + decodedPathname === `/p/all/catalog` || + decodedPathname === `/p/all/catalog/${timeFilterName}` || + decodedPathname === `/p/subscriptions/catalog` || + decodedPathname === `/p/subscriptions/catalog/${timeFilterName}` + ); }; export const isDescriptionView = (pathname: string, params: ParamsType): boolean => { @@ -63,7 +73,7 @@ export const isNotFoundView = (pathname: string, params: ParamsType): boolean => return ( !isAllView(pathname) && !isBoardView(pathname, params) && - !isCatalogView(pathname) && + !isCatalogView(pathname, params) && !isDescriptionView(pathname, params) && !isHomeView(pathname) && !isPendingPostView(pathname, params) && diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index 61361983..6d9a3060 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -8,6 +8,7 @@ import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import useFeedStateString from '../../hooks/use-feed-state-string'; import useReplyModal from '../../hooks/use-reply-modal'; +import useTimeFilter from '../../hooks/use-time-filter'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; import LoadingEllipsis from '../../components/loading-ellipsis'; @@ -42,7 +43,8 @@ const Board = () => { }, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbitAddresses, subscriptions]); const { sortType } = useSortingStore(); - const { feed, hasMore, loadMore, reset } = useFeed({ subplebbitAddresses, sortType }); + const { timeFilterSeconds } = useTimeFilter(); + const { feed, hasMore, loadMore, reset } = useFeed({ subplebbitAddresses, sortType, postsPerPage: 10, newerThan: timeFilterSeconds }); const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { @@ -91,15 +93,15 @@ const Board = () => { const setLastVirtuosoState = () => { virtuosoRef.current?.getState((snapshot: StateSnapshot) => { if (snapshot?.ranges?.length) { - lastVirtuosoStates[location.pathname] = snapshot; + lastVirtuosoStates[sortType + timeFilterSeconds] = snapshot; } }); }; window.addEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState); - }, [location.pathname]); + }, [sortType, timeFilterSeconds]); - const lastVirtuosoState = lastVirtuosoStates?.[location.pathname]; + const lastVirtuosoState = lastVirtuosoStates?.[sortType + timeFilterSeconds]; useEffect(() => { document.title = title ? title : shortAddress || subplebbitAddress; diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index 2a93a836..cf14096e 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -7,6 +7,7 @@ import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import useFeedStateString from '../../hooks/use-feed-state-string'; import { useMultisubMetadata } from '../../hooks/use-default-subplebbits'; +import useTimeFilter from '../../hooks/use-time-filter'; import useWindowWidth from '../../hooks/use-window-width'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; @@ -104,7 +105,8 @@ const Catalog = () => { const postsPerPage = useMemo(() => (columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25), []); const { sortType } = useSortingStore(); - const { feed, hasMore, loadMore, reset } = useFeed({ subplebbitAddresses, sortType, postsPerPage }); + const { timeFilterSeconds } = useTimeFilter(); + const { feed, hasMore, loadMore, reset } = useFeed({ subplebbitAddresses, sortType, postsPerPage, newerThan: timeFilterSeconds }); const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { @@ -147,14 +149,14 @@ const Catalog = () => { const setLastVirtuosoState = () => virtuosoRef.current?.getState((snapshot: StateSnapshot) => { if (snapshot?.ranges?.length) { - lastVirtuosoStates[location.pathname] = snapshot; + lastVirtuosoStates[sortType + timeFilterSeconds + 'catalog'] = snapshot; } }); window.addEventListener('scroll', setLastVirtuosoState); return () => window.removeEventListener('scroll', setLastVirtuosoState); - }, [location.pathname]); + }, [sortType, timeFilterSeconds]); - const lastVirtuosoState = lastVirtuosoStates?.[location.pathname]; + const lastVirtuosoState = lastVirtuosoStates?.[sortType + timeFilterSeconds + 'catalog']; useEffect(() => { let documentTitle = title ? title : shortAddress;