From 882703b00d67b782d1329ee1135dda27113e9aae Mon Sep 17 00:00:00 2001 From: "Tom (plebeius.eth)" Date: Thu, 20 Feb 2025 15:15:19 +0100 Subject: [PATCH] feat: auto subscribe new accounts to specific default subplebbits --- src/components/topbar/topbar.tsx | 54 ++++++++-------------- src/hooks/use-auto-subscribe.ts | 64 ++++++++++++++++++++++++++ src/hooks/use-default-subplebbits.ts | 12 ++++- src/hooks/use-initial-theme.ts | 2 +- src/hooks/use-is-subplebbit-offline.ts | 6 +-- src/hooks/use-theme.ts | 2 +- src/stores/use-auto-subscribe-store.ts | 25 ++++++++++ src/views/board/board.tsx | 35 ++++++++------ src/views/catalog/catalog.tsx | 3 +- src/views/home/home.tsx | 2 +- 10 files changed, 147 insertions(+), 58 deletions(-) create mode 100644 src/hooks/use-auto-subscribe.ts create mode 100644 src/stores/use-auto-subscribe-store.ts diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index 27dc6436..6ac598db 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -5,7 +5,7 @@ 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 useDefaultSubplebbits, { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import _, { debounce } from 'lodash'; import { TimeFilter } from '../board-buttons'; @@ -67,52 +67,36 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => ); }; -const renderBoardsList = (subplebbits: any, isInCatalogView: boolean, subscriptions: string[]) => - subplebbits?.length > 0 && ( - <> - [ - {subplebbits.map((sub: any, index: any) => ( - - {index === 0 ? null : ' '} - - {sub.address.endsWith('.eth') || sub.address.endsWith('.sol') ? sub.address : sub.address.slice(0, 10).concat('...')} - - {index !== subplebbits.length - 1 ? ' /' : null} - - ))} - ]{' '} - {subscriptions?.length > 0 && ( - <> - [ - {subscriptions.map((address: any, index: any) => ( - - {index === 0 ? null : ' '} - - {address.endsWith('.eth') || address.endsWith('.sol') ? address : address.slice(0, 10).concat('...')} - - {index !== subscriptions?.length - 1 ? ' /' : null} - - ))} - ]{' '} - - )} - - ); - const TopBarDesktop = () => { const { t } = useTranslation(); const account = useAccount(); const location = useLocation(); const params = useParams(); const isInCatalogView = isCatalogView(location.pathname, params); - const subplebbits = useDefaultSubplebbits(); const [showSearchBar, setShowSearchBar] = useState(false); + const subscriptions = account?.subscriptions; + return (
[all / subscriptions]{' '} - {renderBoardsList(subplebbits.slice(0, 15), isInCatalogView, account?.subscriptions)}[ + {subscriptions?.length > 0 && ( + <> + [ + {subscriptions.map((address: any, index: any) => ( + + {index === 0 ? null : ' '} + + {address.endsWith('.eth') || address.endsWith('.sol') ? address : address.slice(0, 10).concat('...')} + + {index !== subscriptions?.length - 1 ? ' /' : null} + + ))} + ]{' '} + + )} + [ (); + +export const useAutoSubscribe = () => { + const account = useAccount(); + const accountAddress = account?.author?.address; + const defaultSubplebbits = useDefaultSubplebbits(); + const { addCheckingAccount, removeCheckingAccount, isCheckingAccount } = useAutoSubscribeStore(); + + useEffect(() => { + if (!accountAddress) return; + + // Mark as checking immediately when account changes + addCheckingAccount(accountAddress); + + const processAutoSubscribe = async () => { + if (!account || !defaultSubplebbits?.length) return; + + if (processedAccounts.has(accountAddress)) { + removeCheckingAccount(accountAddress); + return; + } + + const storageKey = AUTO_SUBSCRIBE_KEY_PREFIX + accountAddress; + const hasAutoSubscribed = localStorage.getItem(storageKey); + + if (account.subscriptions?.length > 0 || hasAutoSubscribed) { + processedAccounts.add(accountAddress); + removeCheckingAccount(accountAddress); + return; + } + + const autoSubscribeAddresses = getAutoSubscribeAddresses(); + if (autoSubscribeAddresses.length) { + try { + await setAccount({ + ...account, + subscriptions: autoSubscribeAddresses, + }); + localStorage.setItem(storageKey, 'true'); + processedAccounts.add(accountAddress); + } catch (error) { + console.error('Auto-subscribe error:', error); + } + removeCheckingAccount(accountAddress); + } + }; + + processAutoSubscribe(); + + return () => { + if (accountAddress) removeCheckingAccount(accountAddress); + }; + }, [account, accountAddress, defaultSubplebbits, addCheckingAccount, removeCheckingAccount]); + + return { isCheckingSubscriptions: accountAddress ? isCheckingAccount(accountAddress) : true }; +}; diff --git a/src/hooks/use-default-subplebbits.ts b/src/hooks/use-default-subplebbits.ts index f7aaa81e..d604862e 100644 --- a/src/hooks/use-default-subplebbits.ts +++ b/src/hooks/use-default-subplebbits.ts @@ -12,12 +12,14 @@ export interface MultisubSubplebbit { address: string; tags?: string[]; features?: string[]; + plebchanAutoSubscribe?: boolean; } let cacheSubplebbits: MultisubSubplebbit[] | null = null; let cacheMetadata: MultisubMetadata | null = null; +let cacheAutoSubscribeAddresses: string[] | null = null; -const useDefaultSubplebbits = () => { +export const useDefaultSubplebbits = () => { const [subplebbits, setSubplebbits] = useState([]); useEffect(() => { @@ -31,6 +33,12 @@ const useDefaultSubplebbits = () => { // { cache: 'no-cache' } ).then((res) => res.json()); cacheSubplebbits = multisub.subplebbits; + + // Cache auto-subscribe addresses when we fetch subplebbits + cacheAutoSubscribeAddresses = multisub.subplebbits + .filter((sub: MultisubSubplebbit) => sub.plebchanAutoSubscribe && sub.address) + .map((sub: MultisubSubplebbit) => sub.address); + setSubplebbits(multisub.subplebbits); } catch (e) { console.warn(e); @@ -72,4 +80,4 @@ export const useMultisubMetadata = () => { return cacheMetadata || metadata; }; -export default useDefaultSubplebbits; +export const getAutoSubscribeAddresses = () => cacheAutoSubscribeAddresses || []; diff --git a/src/hooks/use-initial-theme.ts b/src/hooks/use-initial-theme.ts index 9f041663..337ba05d 100644 --- a/src/hooks/use-initial-theme.ts +++ b/src/hooks/use-initial-theme.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import useThemeStore from '../stores/use-theme-store'; -import useDefaultSubplebbits from './use-default-subplebbits'; +import { useDefaultSubplebbits } from './use-default-subplebbits'; import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView } from '../lib/utils/view-utils'; import { nsfwTags } from '../views/home/home'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; diff --git a/src/hooks/use-is-subplebbit-offline.ts b/src/hooks/use-is-subplebbit-offline.ts index f0a98838..50a80023 100644 --- a/src/hooks/use-is-subplebbit-offline.ts +++ b/src/hooks/use-is-subplebbit-offline.ts @@ -26,11 +26,11 @@ const useIsSubplebbitOffline = (subplebbit: Subplebbit) => { const subplebbitOfflineStore = subplebbitOfflineState[address] || { initialLoad: true }; const loadingStartTimestamp = subplebbitsLoadingStartTimestamps[0] || 0; - const isLoading = subplebbitOfflineStore.initialLoad && (!updatedAt || Date.now() / 1000 - updatedAt >= 120 * 60) && Date.now() / 1000 - loadingStartTimestamp < 30; + const isLoading = subplebbitOfflineStore.initialLoad && (!updatedAt || Date.now() / 1000 - updatedAt >= 120 * 120) && Date.now() / 1000 - loadingStartTimestamp < 30; - const isOffline = !isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 120 * 60) || (!updatedAt && Date.now() / 1000 - loadingStartTimestamp >= 30)); + const isOffline = !isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 120 * 120) || (!updatedAt && Date.now() / 1000 - loadingStartTimestamp >= 30)); - const isOnline = updatedAt && Date.now() / 1000 - updatedAt < 120 * 60; + const isOnline = updatedAt && Date.now() / 1000 - updatedAt < 120 * 120; const offlineIconClass = isLoading ? 'yellowOfflineIcon' : isOffline ? 'redOfflineIcon' : ''; const offlineTitle = isLoading diff --git a/src/hooks/use-theme.ts b/src/hooks/use-theme.ts index 346998c0..ac04bc1f 100644 --- a/src/hooks/use-theme.ts +++ b/src/hooks/use-theme.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { isAllView, isSubscriptionsView } from '../lib/utils/view-utils'; import useThemeStore from '../stores/use-theme-store'; -import useDefaultSubplebbits from './use-default-subplebbits'; +import { useDefaultSubplebbits } from './use-default-subplebbits'; import useInitialTheme from './use-initial-theme'; import { nsfwTags } from '../views/home/home'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; diff --git a/src/stores/use-auto-subscribe-store.ts b/src/stores/use-auto-subscribe-store.ts new file mode 100644 index 00000000..c65bceb1 --- /dev/null +++ b/src/stores/use-auto-subscribe-store.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +interface AutoSubscribeState { + checkingAccounts: Set; + addCheckingAccount: (address: string) => void; + removeCheckingAccount: (address: string) => void; + isCheckingAccount: (address: string) => boolean; + reset: () => void; +} + +export const useAutoSubscribeStore = create((set, get) => ({ + checkingAccounts: new Set(), + addCheckingAccount: (address) => + set((state) => ({ + checkingAccounts: new Set(state.checkingAccounts).add(address), + })), + removeCheckingAccount: (address) => + set((state) => { + const newSet = new Set(state.checkingAccounts); + newSet.delete(address); + return { checkingAccounts: newSet }; + }), + isCheckingAccount: (address) => get().checkingAccounts.has(address), + reset: () => set({ checkingAccounts: new Set() }), +})); diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index 9f353f5a..2a62f38b 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -6,7 +6,6 @@ import { Trans, useTranslation } from 'react-i18next'; import styles from './board.module.css'; 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'; @@ -20,6 +19,7 @@ import SubplebbitRules from '../../components/subplebbit-rules'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; import { shouldShowSnow } from '../../lib/snow'; +import { useAutoSubscribe } from '../../hooks/use-auto-subscribe'; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; @@ -31,6 +31,7 @@ const threadsWithoutImagesFilter = (comment: Comment) => { }; const Board = () => { + const { isCheckingSubscriptions } = useAutoSubscribe(); const { t } = useTranslation(); const location = useLocation(); const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>(); @@ -295,18 +296,26 @@ const Board = () => { /> )} {rules && !description && rules.length > 0 && } - } - useWindowScroll={true} - components={{ Footer }} - endReached={loadMore} - ref={virtuosoRef} - restoreStateFrom={lastVirtuosoState} - initialScrollTop={lastVirtuosoState?.scrollTop} - /> + {isCheckingSubscriptions ? ( +
+
+ +
+
+ ) : ( + } + useWindowScroll={true} + components={{ Footer }} + endReached={loadMore} + ref={virtuosoRef} + restoreStateFrom={lastVirtuosoState} + initialScrollTop={lastVirtuosoState?.scrollTop} + /> + )}
); diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index cc612419..79d0f3d4 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -6,8 +6,7 @@ import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; import { getCommentMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; import useCatalogFeedRows from '../../hooks/use-catalog-feed-rows'; -import useDefaultSubplebbits from '../../hooks/use-default-subplebbits'; -import useFeedStateString from '../../hooks/use-feed-state-string'; +import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; import useTimeFilter from '../../hooks/use-time-filter'; import useWindowWidth from '../../hooks/use-window-width'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index 4d592220..83af6cf3 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -4,7 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { useSubplebbits } from '@plebbit/plebbit-react-hooks'; import styles from './home.module.css'; import packageJson from '../../../package.json'; -import useDefaultSubplebbits, { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; +import { useDefaultSubplebbits, useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import useSubplebbitsStats from '../../hooks/use-subplebbits-stats'; import PopularThreadsBox from './popular-threads-box'; import BoardsList from './boards-list';