import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Plebbit from '@plebbit/plebbit-js'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts'; import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits'; import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils'; import { TimeFilter } from '../board-buttons'; import useCreateBoardModalStore from '../../stores/use-create-board-modal-store'; import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store'; import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store'; import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes'; import styles from './topbar.module.css'; import _, { debounce } from 'lodash'; const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => void }) => { const { t } = useTranslation(); const navigate = useNavigate(); const searchBarRef = useRef(null); const searchInputRef = useRef(null); const placeholder = _.lowerCase(t('enter_board_address')); useEffect(() => { searchInputRef.current?.focus(); }, []); const handleClickOutside = useCallback( (event: MouseEvent) => { if (searchBarRef.current && !searchBarRef.current.contains(event.target as Node)) { setShowSearchBar(false); } }, [searchBarRef, setShowSearchBar], ); useEffect(() => { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [handleClickOutside]); useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === 'Escape') { setShowSearchBar(false); } }; document.addEventListener('keydown', handleEscapeKey); return () => { document.removeEventListener('keydown', handleEscapeKey); }; }, [setShowSearchBar]); const handleSearchSubmit = (event: React.FormEvent) => { event.preventDefault(); const searchInput = searchInputRef.current?.value; if (searchInput) { searchInputRef.current.value = ''; navigate(`/${searchInput}`); setShowSearchBar(false); } }; return (
); }; // Helper function to find board address by directory code const findBoardAddressByCode = (code: string, defaultSubplebbits: MultisubSubplebbit[]): string | null => { const entry = defaultSubplebbits.find((subplebbit) => { if (!subplebbit.title) return false; const directory = extractDirectoryFromTitle(subplebbit.title); return directory === code; }); return entry?.address || null; }; const TopBarDesktop = () => { const { t } = useTranslation(); const location = useLocation(); const params = useParams(); const isInCatalogView = isCatalogView(location.pathname, params); const [showSearchBar, setShowSearchBar] = useState(false); const [showAllTemporarily, setShowAllTemporarily] = useState(false); const { openCreateBoardModal } = useCreateBoardModalStore(); const { openTopbarEditModal } = useTopbarEditModalStore(); const { openDirectoryModal } = useDirectoryModalStore(); const { visibleDirectories, visibleSubscriptions } = useTopbarVisibilityStore(); const defaultSubplebbits = useDefaultSubplebbits(); // Memoize allBoardCodes since it's derived from a constant const allBoardCodes = useMemo(() => getAllBoardCodes(), []); // Use accounts store with selective subscriptions to avoid rerenders from updatingState // Only subscribe to subscriptions array and account subplebbit addresses const subscriptions = useAccountsStore( (state) => { const activeAccountId = state.activeAccountId; const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined; // Spread to create new reference - if array is mutated in place, the equality // function needs different references to detect content changes return [...(activeAccount?.subscriptions || [])]; }, (prev, next) => { // Shallow compare arrays - only rerender if subscriptions actually change if (prev.length !== next.length) return false; return prev.every((val, idx) => val === next[idx]); }, ); const accountSubplebbitAddresses = useAccountsStore( (state) => { const activeAccountId = state.activeAccountId; const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined; const accountSubplebbits = activeAccount?.subplebbits || {}; return Object.keys(accountSubplebbits); }, (prev, next) => { // Shallow compare arrays - only rerender if addresses actually change if (prev.length !== next.length) return false; return prev.every((val, idx) => val === next[idx]); }, ); // Filter subscriptions to only show visible ones const visibleSubscriptionAddresses = subscriptions.filter((address: string) => visibleSubscriptions.has(address)); // Check if any directories are hidden const hasHiddenDirectories = useMemo(() => { return allBoardCodes.some((code) => !visibleDirectories.has(code)); }, [allBoardCodes, visibleDirectories]); // Determine which directories to show (all if temporarily showing all, otherwise only visible ones) const directoriesToShow = useMemo(() => { if (showAllTemporarily) { return new Set(allBoardCodes); } return visibleDirectories; }, [showAllTemporarily, visibleDirectories, allBoardCodes]); // Initialize visibility store on mount useEffect(() => { useTopbarVisibilityStore.getState().initialize(); }, []); // Render a board code link or placeholder const renderBoardCode = (code: string, isLastInGroup: boolean) => { const address = findBoardAddressByCode(code, defaultSubplebbits); const isPlaceholder = !address; const handleClick = (e: React.MouseEvent) => { // If no address exists, prevent navigation and open directory modal if (!address) { e.preventDefault(); e.stopPropagation(); openDirectoryModal(); } }; const linkContent = ( <> {isPlaceholder ? ( {code} ) : ( {code} )} ); return ( {linkContent} {!isLastInGroup && ' / '} ); }; // Render a subscription link const renderSubscription = (address: string, index: number, total: number) => { const boardPath = getBoardPath(address, defaultSubplebbits); const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress({ address }); return ( {boardPath && boardPath.trim() ? {displayText} : {displayText}} {index !== total - 1 && ' / '} ); }; return (
[all / subs {accountSubplebbitAddresses.length > 0 && ( <> {' '} / mod )} ]{' '} {BOARD_CODE_GROUPS.map((group, groupIndex) => { const visibleCodes = group.filter((code) => directoriesToShow.has(code)); if (visibleCodes.length === 0) return null; return [{visibleCodes.map((code, codeIndex) => renderBoardCode(code, codeIndex === visibleCodes.length - 1))}] ; })} {hasHiddenDirectories && !showAllTemporarily && ( <> {' '} [ setShowAllTemporarily(true)} style={{ cursor: 'pointer' }} title='Show all'> ... ]{' '} )} {visibleSubscriptionAddresses.length > 0 && ( <>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] )} [ openTopbarEditModal()} style={{ cursor: 'pointer' }}> {_.capitalize(t('edit'))} ] [ openCreateBoardModal()} style={{ cursor: 'pointer' }}> {t('create_board')} ] [{t('settings')}] [ setShowSearchBar(!showSearchBar)}>{t('search')}] [{t('home')}] {showSearchBar && }
); }; const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const { t } = useTranslation(); const navigate = useNavigate(); const defaultSubplebbits = useDefaultSubplebbits(); const displaySubplebbitAddress = subplebbitAddress && subplebbitAddress.length > 30 ? subplebbitAddress.slice(0, 30).concat('...') : subplebbitAddress; const [showSearchBar, setShowSearchBar] = useState(false); // Filter to only show directory boards (those with titles) const directoryBoards = useMemo(() => defaultSubplebbits.filter((sub) => sub.title && extractDirectoryFromTitle(sub.title)), [defaultSubplebbits]); const location = useLocation(); const params = useParams(); const isInAllView = isAllView(location.pathname); const isInCatalogView = isCatalogView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const boardPath = useBoardPath(subplebbitAddress); const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : boardPath || subplebbitAddress; // Use accounts store with selective subscriptions to avoid rerenders from updatingState // Only subscribe to account subplebbit addresses (keys only) const accountSubplebbitAddresses = useAccountsStore( (state) => { const activeAccountId = state.activeAccountId; const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined; const accountSubplebbits = activeAccount?.subplebbits || {}; return Object.keys(accountSubplebbits); }, (prev, next) => { // Shallow compare arrays - only rerender if addresses actually change if (prev.length !== next.length) return false; return prev.every((val, idx) => val === next[idx]); }, ); // Check if current subplebbit is a directory board const currentIsDirectoryBoard = directoryBoards.some((board) => board.address === subplebbitAddress); const boardSelect = ( ); // navbar animation on scroll const [visible, setVisible] = useState(true); const prevScrollPosRef = useRef(0); useEffect(() => { const debouncedHandleScroll = debounce(() => { const currentScrollPos = window.scrollY; const prevScrollPos = prevScrollPosRef.current; setVisible(prevScrollPos > currentScrollPos || currentScrollPos < 10); prevScrollPosRef.current = currentScrollPos; }, 50); window.addEventListener('scroll', debouncedHandleScroll); return () => window.removeEventListener('scroll', debouncedHandleScroll); }, []); return (
{t('board')} {boardSelect} {(isInAllView || isInSubscriptionsView) && ( )}
{t('settings')} setShowSearchBar(!showSearchBar)}>{_.capitalize(t('search'))} {t('home')} {showSearchBar && }
); }; const TopBar = () => { const params = useParams(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const resolvedSubplebbitAddress = useResolvedSubplebbitAddress(); const subplebbitAddress = resolvedSubplebbitAddress || accountComment?.subplebbitAddress; return ( <> ); }; export default TopBar;