import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import getShortAddress from '../../lib/get-short-address'; import { useAccountComment } from '@bitsocialhq/bitsocial-react-hooks'; import useAccountsStore from '@bitsocialhq/bitsocial-react-hooks/dist/stores/accounts'; import { isAllView, isCatalogView, isModView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useAccountSubplebbitAddresses } from '../../hooks/use-account-subplebbit-addresses'; import { useDirectories, useDirectoriesMetadata, DirectoryCommunity } from '../../hooks/use-directories'; import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils'; import useCreateBoardModalStore from '../../stores/use-create-board-modal-store'; import useBoardsBarEditModalStore from '../../stores/use-boards-bar-edit-modal-store'; import useBoardsBarVisibilityStore from '../../stores/use-boards-bar-visibility-store'; import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes'; import styles from './boards-bar.module.css'; import capitalize from 'lodash/capitalize'; import debounce from 'lodash/debounce'; import lowerCase from 'lodash/lowerCase'; import startCase from 'lodash/startCase'; 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, directories: DirectoryCommunity[]): string | null => { const entry = directories.find((subplebbit) => { if (!subplebbit.title) return false; const directory = extractDirectoryFromTitle(subplebbit.title); return directory === code; }); return entry?.address || null; }; const BoardsBarDesktop = () => { 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 { openBoardsBarEditModal } = useBoardsBarEditModalStore(); const { openDirectoryModal } = useDirectoryModalStore(); const { visibleDirectories, showSubscriptionsInBoardsBar } = useBoardsBarVisibilityStore(); const directories = useDirectories(); // Memoize allBoardCodes since it's derived from a constant const allBoardCodes = useMemo(() => getAllBoardCodes(), []); const subscriptions = useAccountsStore( (state) => { const activeAccountId = state.activeAccountId; const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined; return [...(activeAccount?.subscriptions || [])]; }, (prev, next) => { if (prev.length !== next.length) return false; return prev.every((val, idx) => val === next[idx]); }, ); const accountSubplebbitAddresses = useAccountSubplebbitAddresses(); // Show all subscriptions when enabled; no separate per-address tracking (avoids drift when subscribing from board-buttons) const visibleSubscriptionAddresses = showSubscriptionsInBoardsBar ? subscriptions : []; // 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(() => { useBoardsBarVisibilityStore.getState().initialize(); }, []); // Render a board code link or placeholder const renderBoardCode = (code: string, isLastInGroup: boolean) => { const address = findBoardAddressByCode(code, directories); 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 ? ( { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (!address) openDirectoryModal(); } }} onClick={handleClick} style={{ cursor: 'pointer' }} > {code} ) : ( {code} )} ); return ( {linkContent} {!isLastInGroup && ' / '} ); }; // Render a subscription link const renderSubscription = (address: string, index: number, total: number) => { const boardPath = getBoardPath(address, directories); const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : 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 && ( <> {' '} [ { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setShowAllTemporarily(true); } }} onClick={() => setShowAllTemporarily(true)} style={{ cursor: 'pointer' }} title='Show all' > ... ]{' '} )} {visibleSubscriptionAddresses.length > 0 && ( <>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] )} [ { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openBoardsBarEditModal(); } }} onClick={() => openBoardsBarEditModal()} style={{ cursor: 'pointer' }} > {capitalize(t('edit'))} ] [ { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openCreateBoardModal(); } }} onClick={() => openCreateBoardModal()} style={{ cursor: 'pointer' }} > {t('create_board')} ] [{t('settings')}] [ { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setShowSearchBar(!showSearchBar); } }} onClick={() => setShowSearchBar(!showSearchBar)} > {t('search')} ] [{t('home')}] {showSearchBar && }
); }; const BoardsBarMobile = ({ subplebbitAddress }: { subplebbitAddress?: string }) => { const { t } = useTranslation(); const navigate = useNavigate(); const directories = useDirectories(); const directoriesMetadata = useDirectoriesMetadata(); 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(() => directories.filter((sub) => sub.title && extractDirectoryFromTitle(sub.title)), [directories]); const location = useLocation(); const params = useParams(); const isInAllView = isAllView(location.pathname); const isInCatalogView = isCatalogView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInModView = isModView(location.pathname); const boardPath = useBoardPath(subplebbitAddress); const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : isInModView ? 'mod' : boardPath || subplebbitAddress; const accountSubplebbitAddresses = useAccountSubplebbitAddresses(); // Check if current subplebbit is a directory board const currentIsDirectoryBoard = directoryBoards.some((board) => board.address === subplebbitAddress); // Build multiboards with full titles, then combine with directory boards and sort alphabetically const sortedBoardOptions = useMemo(() => { const allTitle = directoriesMetadata?.title || '/all/ - All 5chan Directories'; const subsTitle = '/subs/ - Subscriptions'; const modTitle = `/mod/ - ${startCase(t('boards_you_moderate'))}`; const multiboards: Array<{ value: string; label: string }> = [ { value: 'all', label: allTitle }, { value: 'subs', label: subsTitle }, ...(accountSubplebbitAddresses.length > 0 ? [{ value: 'mod', label: modTitle }] : []), ]; const directoryOptions = directoryBoards.map((board) => { const directoryCode = extractDirectoryFromTitle(board.title!); return { value: directoryCode!, label: board.title! }; }); return [...multiboards, ...directoryOptions].sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })); }, [directoriesMetadata?.title, t, accountSubplebbitAddresses.length, directoryBoards]); 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, { passive: true }); return () => { window.removeEventListener('scroll', debouncedHandleScroll); debouncedHandleScroll.cancel(); }; }, []); return (
{t('board')} {boardSelect}
{t('settings')} { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setShowSearchBar(!showSearchBar); } }} onClick={() => setShowSearchBar(!showSearchBar)} > {capitalize(t('search'))} {t('home')} {showSearchBar && }
); }; const BoardsBar = () => { const params = useParams(); const commentIndex = params?.accountCommentIndex ? parseInt(params.accountCommentIndex) : undefined; const accountComment = useAccountComment({ commentIndex }); const resolvedSubplebbitAddress = useResolvedSubplebbitAddress(); const subplebbitAddress = resolvedSubplebbitAddress || accountComment?.subplebbitAddress; return ( <> ); }; export default BoardsBar;